This commit is contained in:
@@ -7,6 +7,10 @@ releases/
|
|||||||
# Mirrored HRPC/hyperschema output for bare-pack (generated by npm run build:hrpc)
|
# Mirrored HRPC/hyperschema output for bare-pack (generated by npm run build:hrpc)
|
||||||
native-host/spec/
|
native-host/spec/
|
||||||
|
|
||||||
|
# Synced copies of examples/ (source of truth is repo-root examples/)
|
||||||
|
extension/examples/
|
||||||
|
native-host/examples/
|
||||||
|
|
||||||
# Native host: generated launchers (paths are machine-specific)
|
# Native host: generated launchers (paths are machine-specific)
|
||||||
native-host/bridge-swarm-host
|
native-host/bridge-swarm-host
|
||||||
native-host/bridge-swarm-host.bat
|
native-host/bridge-swarm-host.bat
|
||||||
|
|||||||
@@ -87,11 +87,9 @@ npm run build:dist:package # all-platform host zips (needs bare-build; best
|
|||||||
|
|
||||||
### Try the examples
|
### Try the examples
|
||||||
|
|
||||||
```bash
|
In the extension **Settings** (or Dashboard → Settings), enable **Examples server**. Open **http://127.0.0.1:4173/** (do not use `file://`). Pick a demo and open it in two tabs.
|
||||||
npm run examples
|
|
||||||
```
|
|
||||||
|
|
||||||
Opens **http://127.0.0.1:4173/** (do not use `file://` — browsers treat each local file as a unique origin). Pick a demo and open it in two tabs.
|
Dev alternative from the repo: `npm run examples` (same URL).
|
||||||
|
|
||||||
### Your First P2P App
|
### Your First P2P App
|
||||||
|
|
||||||
@@ -326,11 +324,12 @@ Run `npm run build:hrpc` to generate the HRPC spec.
|
|||||||
### Build Commands
|
### Build Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run examples # Serve demos at http://127.0.0.1:4173/ (not file://)
|
npm run examples # Dev: serve demos at http://127.0.0.1:4173/ (or use Settings toggle)
|
||||||
|
npm run sync:examples # Copy examples/ into extension + native-host for packaging
|
||||||
npm run build:protomux # Rebuild Protomux bundle
|
npm run build:protomux # Rebuild Protomux bundle
|
||||||
npm run build:hrpc # Rebuild HRPC spec (+ mirror into native-host/spec)
|
npm run build:hrpc # Rebuild HRPC spec (+ mirror into native-host/spec)
|
||||||
npm run build # host launcher + protomux + hrpc
|
npm run build # host launcher + protomux + hrpc
|
||||||
npm run pack # Package extension → releases/BridgeSwarm-*.zip|.xpi
|
npm run pack # Package extension (includes examples) → releases/
|
||||||
npm run build:dist # Standalone host binary (current platform)
|
npm run build:dist # Standalone host binary (current platform)
|
||||||
npm run build:dist:package # All platforms + zip archives (CI)
|
npm run build:dist:package # All platforms + zip archives (CI)
|
||||||
```
|
```
|
||||||
|
|||||||
+4
-2
@@ -6,13 +6,15 @@ Open these in your browser **after** installing the BridgeSwarm extension and na
|
|||||||
|
|
||||||
## Run the examples server
|
## Run the examples server
|
||||||
|
|
||||||
From the BridgeSwarm repo root:
|
**Recommended:** In the BridgeSwarm extension, open **Settings** (or the Dashboard → Settings) and enable **Examples server**. The native host serves the bundled demos at **http://127.0.0.1:4173/**. Settings save automatically.
|
||||||
|
|
||||||
|
From the BridgeSwarm repo root (dev alternative):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run examples
|
npm run examples
|
||||||
```
|
```
|
||||||
|
|
||||||
Serves `examples/` at **http://127.0.0.1:4173/**. Use two browser tabs for P2P demos.
|
Either way, use **http://127.0.0.1:4173/** (two browser tabs for P2P demos). Do not open `file://` or `chrome-extension://` copies for demos that need BridgeSwarm injection.
|
||||||
|
|
||||||
Shared chrome lives in [`shared/`](shared/) (`theme.css`, `chrome.css`, `boot.js`).
|
Shared chrome lives in [`shared/`](shared/) (`theme.css`, `chrome.css`, `boot.js`).
|
||||||
|
|
||||||
|
|||||||
+95
-6
@@ -55,7 +55,15 @@ const swarmRefCount = new Map();
|
|||||||
const activeConnections = new Map();
|
const activeConnections = new Map();
|
||||||
|
|
||||||
const SETTINGS_KEY = 'bridgeSwarmSettings';
|
const SETTINGS_KEY = 'bridgeSwarmSettings';
|
||||||
|
const EXAMPLES_URL = 'http://127.0.0.1:4173/';
|
||||||
let notifyOnDisconnect = false;
|
let notifyOnDisconnect = false;
|
||||||
|
let examplesServerEnabled = false;
|
||||||
|
let examplesServerState = {
|
||||||
|
running: false,
|
||||||
|
url: null,
|
||||||
|
error: null,
|
||||||
|
rootFound: null,
|
||||||
|
};
|
||||||
|
|
||||||
// Extension state for the UI
|
// Extension state for the UI
|
||||||
const extensionState = {
|
const extensionState = {
|
||||||
@@ -92,17 +100,67 @@ function updateExtensionState() {
|
|||||||
extensionState.stats.totalSwarms = extensionState.swarms.size;
|
extensionState.stats.totalSwarms = extensionState.swarms.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadNotifySetting() {
|
function applySettingsFromStorage(s) {
|
||||||
browser.storage.local.get(SETTINGS_KEY, (result) => {
|
|
||||||
const s = result[SETTINGS_KEY] || {};
|
|
||||||
notifyOnDisconnect = s.notifyOnDisconnect === true;
|
notifyOnDisconnect = s.notifyOnDisconnect === true;
|
||||||
|
const wantExamples = s.examplesServerEnabled === true;
|
||||||
|
if (wantExamples !== examplesServerEnabled) {
|
||||||
|
examplesServerEnabled = wantExamples;
|
||||||
|
syncExamplesServer().catch((err) => log('examples server sync failed:', err.message || err));
|
||||||
|
} else if (wantExamples && port) {
|
||||||
|
syncExamplesServer().catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSettings() {
|
||||||
|
browser.storage.local.get(SETTINGS_KEY, (result) => {
|
||||||
|
applySettingsFromStorage(result[SETTINGS_KEY] || {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function syncExamplesServer() {
|
||||||
|
if (!port) {
|
||||||
|
examplesServerState = {
|
||||||
|
running: false,
|
||||||
|
url: examplesServerEnabled ? EXAMPLES_URL : null,
|
||||||
|
error: examplesServerEnabled ? 'Native host not connected' : null,
|
||||||
|
rootFound: null,
|
||||||
|
};
|
||||||
|
return examplesServerState;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (examplesServerEnabled) {
|
||||||
|
const res = await send({ type: 'examplesServer.start', payload: { host: '127.0.0.1', port: 4173 } });
|
||||||
|
examplesServerState = {
|
||||||
|
running: !!res?.running,
|
||||||
|
url: res?.url || EXAMPLES_URL,
|
||||||
|
error: res?.ok === false ? res.error || 'Failed to start' : null,
|
||||||
|
rootFound: res?.rootFound,
|
||||||
|
};
|
||||||
|
if (res?.ok !== false) log('Examples server:', examplesServerState.url);
|
||||||
|
} else {
|
||||||
|
const res = await send({ type: 'examplesServer.stop', payload: {} });
|
||||||
|
examplesServerState = {
|
||||||
|
running: false,
|
||||||
|
url: null,
|
||||||
|
error: res?.ok === false ? res.error || null : null,
|
||||||
|
rootFound: res?.rootFound,
|
||||||
|
};
|
||||||
|
log('Examples server stopped');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
examplesServerState = {
|
||||||
|
running: false,
|
||||||
|
url: examplesServerEnabled ? EXAMPLES_URL : null,
|
||||||
|
error: err.message || String(err),
|
||||||
|
rootFound: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return examplesServerState;
|
||||||
|
}
|
||||||
|
|
||||||
browser.storage.onChanged.addListener((changes, areaName) => {
|
browser.storage.onChanged.addListener((changes, areaName) => {
|
||||||
if (areaName === 'local' && changes[SETTINGS_KEY]) {
|
if (areaName === 'local' && changes[SETTINGS_KEY]) {
|
||||||
const s = changes[SETTINGS_KEY].newValue || {};
|
applySettingsFromStorage(changes[SETTINGS_KEY].newValue || {});
|
||||||
notifyOnDisconnect = s.notifyOnDisconnect === true;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -117,6 +175,9 @@ function connect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reconnectDelay = INITIAL_RECONNECT_DELAY;
|
reconnectDelay = INITIAL_RECONNECT_DELAY;
|
||||||
|
if (examplesServerEnabled) {
|
||||||
|
syncExamplesServer().catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
port.onMessage.addListener((msg) => {
|
port.onMessage.addListener((msg) => {
|
||||||
log('Received from native:', msg.type, msg.event || '');
|
log('Received from native:', msg.type, msg.event || '');
|
||||||
@@ -195,6 +256,12 @@ function connect() {
|
|||||||
activeConnections.clear();
|
activeConnections.clear();
|
||||||
updateExtensionState();
|
updateExtensionState();
|
||||||
port = null;
|
port = null;
|
||||||
|
examplesServerState = {
|
||||||
|
running: false,
|
||||||
|
url: examplesServerEnabled ? EXAMPLES_URL : null,
|
||||||
|
error: examplesServerEnabled ? 'Native host disconnected' : null,
|
||||||
|
rootFound: examplesServerState.rootFound,
|
||||||
|
};
|
||||||
if (notifyOnDisconnect && browser.notifications) {
|
if (notifyOnDisconnect && browser.notifications) {
|
||||||
browser.notifications.create('bridgeswarm-host-disconnect', {
|
browser.notifications.create('bridgeswarm-host-disconnect', {
|
||||||
type: 'basic',
|
type: 'basic',
|
||||||
@@ -243,7 +310,7 @@ function send(msg) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadNotifySetting();
|
loadSettings();
|
||||||
connect();
|
connect();
|
||||||
|
|
||||||
// Handle tab closure - destroy swarms only when last tab using them closes
|
// Handle tab closure - destroy swarms only when last tab using them closes
|
||||||
@@ -357,6 +424,28 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
.catch((err) => sendResponse({ ok: false, error: err.message }));
|
.catch((err) => sendResponse({ ok: false, error: err.message }));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.action === 'examplesServerStatus') {
|
||||||
|
syncExamplesServer()
|
||||||
|
.then((state) =>
|
||||||
|
sendResponse({
|
||||||
|
ok: !state.error,
|
||||||
|
enabled: examplesServerEnabled,
|
||||||
|
...state,
|
||||||
|
url: state.url || (examplesServerEnabled ? EXAMPLES_URL : null),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch((err) =>
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
enabled: examplesServerEnabled,
|
||||||
|
running: false,
|
||||||
|
url: examplesServerEnabled ? EXAMPLES_URL : null,
|
||||||
|
error: err.message,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Open dashboard when extension icon is clicked
|
// Open dashboard when extension icon is clicked
|
||||||
|
|||||||
@@ -665,9 +665,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 1.5rem;">
|
<div class="settings-group">
|
||||||
<button class="btn btn-primary" id="btnSaveSettings">Save Settings</button>
|
<h3>Examples</h3>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info">
|
||||||
|
<div class="setting-label">Examples Server</div>
|
||||||
|
<div class="setting-desc">Serve bundled demos at http://127.0.0.1:4173/</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="toggle" id="toggleExamplesServer" data-setting="examplesServerEnabled"></div>
|
||||||
|
</div>
|
||||||
|
<div id="examplesServerPanel" style="display: none; margin-top: 0.75rem; padding: 0.75rem; background: #18181b; border-radius: 8px; border: 1px solid #3f3f46;">
|
||||||
|
<div style="font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.85rem; color: #93c5fd; word-break: break-all;" id="examplesServerUrl">http://127.0.0.1:4173/</div>
|
||||||
|
<div style="font-size: 0.8rem; color: #71717a; margin: 0.4rem 0 0.75rem;" id="examplesServerStatus">Off</div>
|
||||||
|
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||||
|
<button class="btn btn-primary btn-sm" id="btnOpenExamples">Open examples</button>
|
||||||
|
<button class="btn btn-secondary btn-sm" id="btnCopyExamplesUrl">Copy URL</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top: 1.25rem; font-size: 0.8rem; color: #71717a;">Settings save automatically when you change them.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+75
-6
@@ -60,38 +60,87 @@ async function fetchState() {
|
|||||||
|
|
||||||
// Load settings
|
// Load settings
|
||||||
function loadSettings() {
|
function loadSettings() {
|
||||||
|
settingsLoading = true;
|
||||||
chrome.storage.local.get(SETTINGS_KEY, (result) => {
|
chrome.storage.local.get(SETTINGS_KEY, (result) => {
|
||||||
settings = result[SETTINGS_KEY] || {};
|
settings = result[SETTINGS_KEY] || {};
|
||||||
updateSettingsUI();
|
updateSettingsUI();
|
||||||
|
settingsLoading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EXAMPLES_URL = 'http://127.0.0.1:4173/';
|
||||||
|
let settingsSaveTimer = null;
|
||||||
|
let settingsLoading = false;
|
||||||
|
|
||||||
// Update settings UI
|
// Update settings UI
|
||||||
function updateSettingsUI() {
|
function updateSettingsUI() {
|
||||||
$('toggleNotify').classList.toggle('active', settings.notifyOnDisconnect === true);
|
$('toggleNotify').classList.toggle('active', settings.notifyOnDisconnect === true);
|
||||||
$('toggleDebug').classList.toggle('active', settings.debug === true);
|
$('toggleDebug').classList.toggle('active', settings.debug === true);
|
||||||
$('toggleDisableFileUrls')?.classList.toggle('active', settings.disableOnFileUrls === true);
|
$('toggleDisableFileUrls')?.classList.toggle('active', settings.disableOnFileUrls === true);
|
||||||
|
$('toggleExamplesServer')?.classList.toggle('active', settings.examplesServerEnabled === true);
|
||||||
$('defaultAppName').value = settings.defaultAppName || 'bridge-swarm';
|
$('defaultAppName').value = settings.defaultAppName || 'bridge-swarm';
|
||||||
$('defaultMaxPeers').value = settings.defaultMaxPeers || '';
|
$('defaultMaxPeers').value = settings.defaultMaxPeers || '';
|
||||||
$('defaultRequestTimeoutMs').value = settings.defaultRequestTimeoutMs || '';
|
$('defaultRequestTimeoutMs').value = settings.defaultRequestTimeoutMs || '';
|
||||||
$('readyTimeoutMs').value = settings.readyTimeoutMs || '';
|
$('readyTimeoutMs').value = settings.readyTimeoutMs || '';
|
||||||
|
updateExamplesServerPanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save settings
|
function updateExamplesServerPanel() {
|
||||||
function saveSettings() {
|
const panel = $('examplesServerPanel');
|
||||||
|
const enabled = settings.examplesServerEnabled === true;
|
||||||
|
if (panel) panel.style.display = enabled ? 'block' : 'none';
|
||||||
|
const urlEl = $('examplesServerUrl');
|
||||||
|
const statusEl = $('examplesServerStatus');
|
||||||
|
if (urlEl) urlEl.textContent = EXAMPLES_URL;
|
||||||
|
if (!enabled) {
|
||||||
|
if (statusEl) statusEl.textContent = 'Off';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (statusEl) statusEl.textContent = 'Starting…';
|
||||||
|
chrome.runtime.sendMessage(
|
||||||
|
{ target: 'bridge-swarm-native', action: 'examplesServerStatus' },
|
||||||
|
(res) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
if (statusEl) statusEl.textContent = chrome.runtime.lastError.message;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!res) {
|
||||||
|
if (statusEl) statusEl.textContent = 'No response';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (urlEl && res.url) urlEl.textContent = res.url;
|
||||||
|
if (!statusEl) return;
|
||||||
|
if (res.error) statusEl.textContent = res.error;
|
||||||
|
else if (res.running) statusEl.textContent = 'Running at ' + (res.url || EXAMPLES_URL);
|
||||||
|
else if (res.enabled) statusEl.textContent = 'Enabled — waiting for native host…';
|
||||||
|
else statusEl.textContent = 'Off';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save settings (autosave)
|
||||||
|
function saveSettings(quiet) {
|
||||||
settings.notifyOnDisconnect = $('toggleNotify').classList.contains('active');
|
settings.notifyOnDisconnect = $('toggleNotify').classList.contains('active');
|
||||||
settings.debug = $('toggleDebug').classList.contains('active');
|
settings.debug = $('toggleDebug').classList.contains('active');
|
||||||
settings.disableOnFileUrls = $('toggleDisableFileUrls')?.classList.contains('active') || false;
|
settings.disableOnFileUrls = $('toggleDisableFileUrls')?.classList.contains('active') || false;
|
||||||
|
settings.examplesServerEnabled = $('toggleExamplesServer')?.classList.contains('active') || false;
|
||||||
settings.defaultAppName = $('defaultAppName').value || 'bridge-swarm';
|
settings.defaultAppName = $('defaultAppName').value || 'bridge-swarm';
|
||||||
settings.defaultMaxPeers = parseInt($('defaultMaxPeers').value) || 0;
|
settings.defaultMaxPeers = parseInt($('defaultMaxPeers').value) || 0;
|
||||||
settings.defaultRequestTimeoutMs = parseInt($('defaultRequestTimeoutMs').value) || 0;
|
settings.defaultRequestTimeoutMs = parseInt($('defaultRequestTimeoutMs').value) || 0;
|
||||||
settings.readyTimeoutMs = parseInt($('readyTimeoutMs').value) || 0;
|
settings.readyTimeoutMs = parseInt($('readyTimeoutMs').value) || 0;
|
||||||
|
|
||||||
chrome.storage.local.set({ [SETTINGS_KEY]: settings }, () => {
|
chrome.storage.local.set({ [SETTINGS_KEY]: settings }, () => {
|
||||||
log('Settings saved');
|
if (!quiet) log('Settings saved');
|
||||||
|
updateExamplesServerPanel();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scheduleSaveSettings() {
|
||||||
|
if (settingsLoading) return;
|
||||||
|
clearTimeout(settingsSaveTimer);
|
||||||
|
settingsSaveTimer = setTimeout(() => saveSettings(true), 250);
|
||||||
|
}
|
||||||
|
|
||||||
// Update dashboard
|
// Update dashboard
|
||||||
function updateDashboard(state) {
|
function updateDashboard(state) {
|
||||||
currentState = state;
|
currentState = state;
|
||||||
@@ -261,15 +310,35 @@ function setupEvents() {
|
|||||||
// Refresh button
|
// Refresh button
|
||||||
$('refreshBtn').addEventListener('click', refresh);
|
$('refreshBtn').addEventListener('click', refresh);
|
||||||
|
|
||||||
// Toggle switches
|
// Toggle switches — autosave on change
|
||||||
document.querySelectorAll('.toggle').forEach(toggle => {
|
document.querySelectorAll('.toggle').forEach(toggle => {
|
||||||
toggle.addEventListener('click', () => {
|
toggle.addEventListener('click', () => {
|
||||||
toggle.classList.toggle('active');
|
toggle.classList.toggle('active');
|
||||||
|
scheduleSaveSettings();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save settings
|
['defaultAppName', 'defaultMaxPeers', 'defaultRequestTimeoutMs', 'readyTimeoutMs'].forEach((id) => {
|
||||||
$('btnSaveSettings').addEventListener('click', saveSettings);
|
const el = $(id);
|
||||||
|
if (!el) return;
|
||||||
|
el.addEventListener('change', scheduleSaveSettings);
|
||||||
|
el.addEventListener('input', scheduleSaveSettings);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('btnOpenExamples')?.addEventListener('click', () => {
|
||||||
|
const url = $('examplesServerUrl')?.textContent || EXAMPLES_URL;
|
||||||
|
chrome.tabs.create({ url });
|
||||||
|
});
|
||||||
|
|
||||||
|
$('btnCopyExamplesUrl')?.addEventListener('click', async () => {
|
||||||
|
const url = $('examplesServerUrl')?.textContent || EXAMPLES_URL;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(url);
|
||||||
|
log('Examples URL copied');
|
||||||
|
} catch (_) {
|
||||||
|
log('Could not copy examples URL');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Logs functionality
|
// Logs functionality
|
||||||
let logs = [];
|
let logs = [];
|
||||||
|
|||||||
@@ -36,7 +36,9 @@
|
|||||||
"protomux-bundle.js",
|
"protomux-bundle.js",
|
||||||
"defaults.js",
|
"defaults.js",
|
||||||
"dashboard.html",
|
"dashboard.html",
|
||||||
"dashboard.js"
|
"dashboard.js",
|
||||||
|
"examples/*",
|
||||||
|
"examples/**/*"
|
||||||
],
|
],
|
||||||
"matches": [
|
"matches": [
|
||||||
"<all_urls>"
|
"<all_urls>"
|
||||||
|
|||||||
@@ -35,7 +35,9 @@
|
|||||||
"protomux-bundle.js",
|
"protomux-bundle.js",
|
||||||
"defaults.js",
|
"defaults.js",
|
||||||
"dashboard.html",
|
"dashboard.html",
|
||||||
"dashboard.js"
|
"dashboard.js",
|
||||||
|
"examples/*",
|
||||||
|
"examples/**/*"
|
||||||
],
|
],
|
||||||
"matches": [
|
"matches": [
|
||||||
"<all_urls>"
|
"<all_urls>"
|
||||||
|
|||||||
+40
-2
@@ -75,11 +75,30 @@
|
|||||||
}
|
}
|
||||||
.toast.show { opacity: 1; }
|
.toast.show { opacity: 1; }
|
||||||
.toast.err { color: #f7768e; }
|
.toast.err { color: #f7768e; }
|
||||||
|
.examples-panel {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
padding: 0.75rem 0.9rem;
|
||||||
|
background: #24283b;
|
||||||
|
border: 1px solid #3b4261;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.examples-panel[hidden] { display: none !important; }
|
||||||
|
.examples-url {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #7aa2f7;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.examples-status { font-size: 0.8rem; color: #565f89; margin: 0.4rem 0 0.75rem; }
|
||||||
|
.examples-status.ok { color: #9ece6a; }
|
||||||
|
.examples-status.err { color: #f7768e; }
|
||||||
|
.examples-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
||||||
|
.autosave-hint { font-size: 0.8rem; color: #565f89; margin-top: 0.25rem; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>BridgeSwarm</h1>
|
<h1>BridgeSwarm</h1>
|
||||||
<p class="sub">Extension settings. Changes apply to new swarms and requests.</p>
|
<p class="sub">Extension settings. Changes save automatically and apply to new swarms and requests.</p>
|
||||||
|
|
||||||
<p style="margin-bottom: 1.5rem;">
|
<p style="margin-bottom: 1.5rem;">
|
||||||
<a href="dashboard.html" target="_blank" style="color: #7aa2f7; text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem;">
|
<a href="dashboard.html" target="_blank" style="color: #7aa2f7; text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem;">
|
||||||
@@ -145,6 +164,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Examples</h2>
|
||||||
|
<div class="field">
|
||||||
|
<div class="checkbox-row">
|
||||||
|
<input type="checkbox" id="examplesServerEnabled" aria-describedby="examplesHint">
|
||||||
|
<label for="examplesServerEnabled">Enable examples server</label>
|
||||||
|
</div>
|
||||||
|
<p class="hint" id="examplesHint">Starts a local HTTP server (via the native host) that serves the bundled demos. Open the URL below in a normal tab so BridgeSwarm can inject.</p>
|
||||||
|
<div class="examples-panel" id="examplesPanel" hidden>
|
||||||
|
<div>Open: <a class="examples-url" id="examplesUrl" href="http://127.0.0.1:4173/" target="_blank" rel="noopener">http://127.0.0.1:4173/</a></div>
|
||||||
|
<p class="examples-status" id="examplesStatus">Off</p>
|
||||||
|
<div class="examples-actions">
|
||||||
|
<button type="button" class="primary" id="btnOpenExamples">Open examples</button>
|
||||||
|
<button type="button" class="secondary" id="btnCopyExamplesUrl">Copy URL</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Debug</h2>
|
<h2>Debug</h2>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -157,10 +195,10 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" class="primary" id="btnSave">Save</button>
|
|
||||||
<button type="button" class="secondary" id="btnReset">Reset to defaults</button>
|
<button type="button" class="secondary" id="btnReset">Reset to defaults</button>
|
||||||
<span class="toast" id="toast"></span>
|
<span class="toast" id="toast"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="autosave-hint">Settings save as you change them.</p>
|
||||||
|
|
||||||
<script src="options.js"></script>
|
<script src="options.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+118
-7
@@ -1,6 +1,7 @@
|
|||||||
(function () {
|
(function () {
|
||||||
const browser = typeof chrome !== 'undefined' && chrome.storage ? chrome : typeof browser !== 'undefined' ? browser : chrome;
|
const browser = typeof chrome !== 'undefined' && chrome.storage ? chrome : typeof browser !== 'undefined' ? browser : chrome;
|
||||||
const KEY = 'bridgeSwarmSettings';
|
const KEY = 'bridgeSwarmSettings';
|
||||||
|
const EXAMPLES_URL = 'http://127.0.0.1:4173/';
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
defaultAppName: 'bridge-swarm',
|
defaultAppName: 'bridge-swarm',
|
||||||
defaultRequestTimeoutMs: 0,
|
defaultRequestTimeoutMs: 0,
|
||||||
@@ -8,7 +9,8 @@
|
|||||||
readyTimeoutMs: 0,
|
readyTimeoutMs: 0,
|
||||||
disableOnFileUrls: false,
|
disableOnFileUrls: false,
|
||||||
notifyOnDisconnect: false,
|
notifyOnDisconnect: false,
|
||||||
debug: false
|
debug: false,
|
||||||
|
examplesServerEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultAppNameEl = document.getElementById('defaultAppName');
|
const defaultAppNameEl = document.getElementById('defaultAppName');
|
||||||
@@ -18,10 +20,18 @@
|
|||||||
const disableOnFileUrlsEl = document.getElementById('disableOnFileUrls');
|
const disableOnFileUrlsEl = document.getElementById('disableOnFileUrls');
|
||||||
const notifyOnDisconnectEl = document.getElementById('notifyOnDisconnect');
|
const notifyOnDisconnectEl = document.getElementById('notifyOnDisconnect');
|
||||||
const debugEl = document.getElementById('debug');
|
const debugEl = document.getElementById('debug');
|
||||||
const btnSave = document.getElementById('btnSave');
|
const examplesServerEnabledEl = document.getElementById('examplesServerEnabled');
|
||||||
|
const examplesPanelEl = document.getElementById('examplesPanel');
|
||||||
|
const examplesUrlEl = document.getElementById('examplesUrl');
|
||||||
|
const examplesStatusEl = document.getElementById('examplesStatus');
|
||||||
|
const btnOpenExamples = document.getElementById('btnOpenExamples');
|
||||||
|
const btnCopyExamplesUrl = document.getElementById('btnCopyExamplesUrl');
|
||||||
const btnReset = document.getElementById('btnReset');
|
const btnReset = document.getElementById('btnReset');
|
||||||
const toastEl = document.getElementById('toast');
|
const toastEl = document.getElementById('toast');
|
||||||
|
|
||||||
|
let saveTimer = null;
|
||||||
|
let loading = false;
|
||||||
|
|
||||||
function showToast(msg, isError) {
|
function showToast(msg, isError) {
|
||||||
toastEl.textContent = msg;
|
toastEl.textContent = msg;
|
||||||
toastEl.className = 'toast show' + (isError ? ' err' : '');
|
toastEl.className = 'toast show' + (isError ? ' err' : '');
|
||||||
@@ -31,16 +41,69 @@
|
|||||||
}, 2500);
|
}, 2500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateExamplesPanel() {
|
||||||
|
const on = examplesServerEnabledEl.checked;
|
||||||
|
examplesPanelEl.hidden = !on;
|
||||||
|
examplesUrlEl.textContent = EXAMPLES_URL;
|
||||||
|
examplesUrlEl.href = EXAMPLES_URL;
|
||||||
|
if (!on) {
|
||||||
|
examplesStatusEl.textContent = 'Off';
|
||||||
|
examplesStatusEl.className = 'examples-status';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
examplesStatusEl.textContent = 'Starting…';
|
||||||
|
examplesStatusEl.className = 'examples-status';
|
||||||
|
browser.runtime.sendMessage(
|
||||||
|
{ target: 'bridge-swarm-native', action: 'examplesServerStatus' },
|
||||||
|
function (res) {
|
||||||
|
if (browser.runtime.lastError) {
|
||||||
|
examplesStatusEl.textContent = browser.runtime.lastError.message;
|
||||||
|
examplesStatusEl.className = 'examples-status err';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!res) {
|
||||||
|
examplesStatusEl.textContent = 'No response from background';
|
||||||
|
examplesStatusEl.className = 'examples-status err';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.error) {
|
||||||
|
examplesStatusEl.textContent = res.error;
|
||||||
|
examplesStatusEl.className = 'examples-status err';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.running) {
|
||||||
|
examplesStatusEl.textContent = 'Running at ' + (res.url || EXAMPLES_URL);
|
||||||
|
examplesStatusEl.className = 'examples-status ok';
|
||||||
|
} else if (res.enabled) {
|
||||||
|
examplesStatusEl.textContent = 'Enabled — waiting for native host…';
|
||||||
|
examplesStatusEl.className = 'examples-status';
|
||||||
|
} else {
|
||||||
|
examplesStatusEl.textContent = 'Off';
|
||||||
|
examplesStatusEl.className = 'examples-status';
|
||||||
|
}
|
||||||
|
if (res.url) {
|
||||||
|
examplesUrlEl.textContent = res.url;
|
||||||
|
examplesUrlEl.href = res.url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function load() {
|
function load() {
|
||||||
|
loading = true;
|
||||||
browser.storage.local.get(KEY, function (result) {
|
browser.storage.local.get(KEY, function (result) {
|
||||||
const s = result[KEY] || {};
|
const s = result[KEY] || {};
|
||||||
defaultAppNameEl.value = s.defaultAppName != null ? s.defaultAppName : DEFAULTS.defaultAppName;
|
defaultAppNameEl.value = s.defaultAppName != null ? s.defaultAppName : DEFAULTS.defaultAppName;
|
||||||
defaultRequestTimeoutMsEl.value = s.defaultRequestTimeoutMs != null ? s.defaultRequestTimeoutMs : DEFAULTS.defaultRequestTimeoutMs;
|
defaultRequestTimeoutMsEl.value =
|
||||||
|
s.defaultRequestTimeoutMs != null ? s.defaultRequestTimeoutMs : DEFAULTS.defaultRequestTimeoutMs;
|
||||||
defaultMaxPeersEl.value = s.defaultMaxPeers != null ? s.defaultMaxPeers : DEFAULTS.defaultMaxPeers;
|
defaultMaxPeersEl.value = s.defaultMaxPeers != null ? s.defaultMaxPeers : DEFAULTS.defaultMaxPeers;
|
||||||
readyTimeoutMsEl.value = s.readyTimeoutMs != null ? s.readyTimeoutMs : DEFAULTS.readyTimeoutMs;
|
readyTimeoutMsEl.value = s.readyTimeoutMs != null ? s.readyTimeoutMs : DEFAULTS.readyTimeoutMs;
|
||||||
disableOnFileUrlsEl.checked = s.disableOnFileUrls === true;
|
disableOnFileUrlsEl.checked = s.disableOnFileUrls === true;
|
||||||
notifyOnDisconnectEl.checked = s.notifyOnDisconnect === true;
|
notifyOnDisconnectEl.checked = s.notifyOnDisconnect === true;
|
||||||
debugEl.checked = s.debug === true;
|
debugEl.checked = s.debug === true;
|
||||||
|
examplesServerEnabledEl.checked = s.examplesServerEnabled === true;
|
||||||
|
loading = false;
|
||||||
|
updateExamplesPanel();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,11 +139,12 @@
|
|||||||
readyTimeoutMs: readyTimeout,
|
readyTimeoutMs: readyTimeout,
|
||||||
disableOnFileUrls: disableOnFileUrlsEl.checked,
|
disableOnFileUrls: disableOnFileUrlsEl.checked,
|
||||||
notifyOnDisconnect: notifyOnDisconnectEl.checked,
|
notifyOnDisconnect: notifyOnDisconnectEl.checked,
|
||||||
debug: debugEl.checked
|
debug: debugEl.checked,
|
||||||
|
examplesServerEnabled: examplesServerEnabledEl.checked,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
btnSave.addEventListener('click', function () {
|
function saveNow(quiet) {
|
||||||
const s = validate();
|
const s = validate();
|
||||||
if (!s) return;
|
if (!s) return;
|
||||||
browser.storage.local.set({ [KEY]: s }, function () {
|
browser.storage.local.set({ [KEY]: s }, function () {
|
||||||
@@ -88,8 +152,53 @@
|
|||||||
showToast('Save failed: ' + browser.runtime.lastError.message, true);
|
showToast('Save failed: ' + browser.runtime.lastError.message, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showToast('Saved.');
|
if (!quiet) showToast('Saved.');
|
||||||
|
updateExamplesPanel();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleSave() {
|
||||||
|
if (loading) return;
|
||||||
|
clearTimeout(saveTimer);
|
||||||
|
saveTimer = setTimeout(function () {
|
||||||
|
saveNow(true);
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputs = [
|
||||||
|
defaultAppNameEl,
|
||||||
|
defaultRequestTimeoutMsEl,
|
||||||
|
defaultMaxPeersEl,
|
||||||
|
readyTimeoutMsEl,
|
||||||
|
disableOnFileUrlsEl,
|
||||||
|
notifyOnDisconnectEl,
|
||||||
|
debugEl,
|
||||||
|
examplesServerEnabledEl,
|
||||||
|
];
|
||||||
|
for (const el of inputs) {
|
||||||
|
el.addEventListener('change', scheduleSave);
|
||||||
|
if (el.tagName === 'INPUT' && (el.type === 'text' || el.type === 'number')) {
|
||||||
|
el.addEventListener('input', scheduleSave);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
examplesServerEnabledEl.addEventListener('change', function () {
|
||||||
|
updateExamplesPanel();
|
||||||
|
});
|
||||||
|
|
||||||
|
btnOpenExamples.addEventListener('click', function () {
|
||||||
|
const url = examplesUrlEl.href || EXAMPLES_URL;
|
||||||
|
browser.tabs.create({ url: url });
|
||||||
|
});
|
||||||
|
|
||||||
|
btnCopyExamplesUrl.addEventListener('click', async function () {
|
||||||
|
const url = examplesUrlEl.textContent || EXAMPLES_URL;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(url);
|
||||||
|
showToast('URL copied.');
|
||||||
|
} catch (_) {
|
||||||
|
showToast('Could not copy URL.', true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
btnReset.addEventListener('click', function () {
|
btnReset.addEventListener('click', function () {
|
||||||
@@ -100,12 +209,14 @@
|
|||||||
disableOnFileUrlsEl.checked = DEFAULTS.disableOnFileUrls;
|
disableOnFileUrlsEl.checked = DEFAULTS.disableOnFileUrls;
|
||||||
notifyOnDisconnectEl.checked = DEFAULTS.notifyOnDisconnect;
|
notifyOnDisconnectEl.checked = DEFAULTS.notifyOnDisconnect;
|
||||||
debugEl.checked = DEFAULTS.debug;
|
debugEl.checked = DEFAULTS.debug;
|
||||||
browser.storage.local.set({ [KEY]: DEFAULTS }, function () {
|
examplesServerEnabledEl.checked = DEFAULTS.examplesServerEnabled;
|
||||||
|
browser.storage.local.set({ [KEY]: Object.assign({}, DEFAULTS) }, function () {
|
||||||
if (browser.runtime.lastError) {
|
if (browser.runtime.lastError) {
|
||||||
showToast('Reset failed: ' + browser.runtime.lastError.message, true);
|
showToast('Reset failed: ' + browser.runtime.lastError.message, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showToast('Reset to defaults.');
|
showToast('Reset to defaults.');
|
||||||
|
updateExamplesPanel();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
/**
|
||||||
|
* Local static HTTP server for BridgeSwarm examples (127.0.0.1).
|
||||||
|
* Started/stopped via native messaging (examplesServer.start / stop / status).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const http = require('bare-http1');
|
||||||
|
const fs = require('bare-fs');
|
||||||
|
const path = require('bare-path');
|
||||||
|
|
||||||
|
const DEFAULT_HOST = '127.0.0.1';
|
||||||
|
const DEFAULT_PORT = 4173;
|
||||||
|
|
||||||
|
const MIME = {
|
||||||
|
'.html': 'text/html; charset=utf-8',
|
||||||
|
'.js': 'text/javascript; charset=utf-8',
|
||||||
|
'.mjs': 'text/javascript; charset=utf-8',
|
||||||
|
'.css': 'text/css; charset=utf-8',
|
||||||
|
'.json': 'application/json; charset=utf-8',
|
||||||
|
'.svg': 'image/svg+xml',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.jpeg': 'image/jpeg',
|
||||||
|
'.gif': 'image/gif',
|
||||||
|
'.webp': 'image/webp',
|
||||||
|
'.ico': 'image/x-icon',
|
||||||
|
'.woff': 'font/woff',
|
||||||
|
'.woff2': 'font/woff2',
|
||||||
|
'.map': 'application/json',
|
||||||
|
'.txt': 'text/plain; charset=utf-8',
|
||||||
|
'.md': 'text/markdown; charset=utf-8',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {import('bare-http1').Server | null} */
|
||||||
|
let server = null;
|
||||||
|
let boundHost = DEFAULT_HOST;
|
||||||
|
let boundPort = DEFAULT_PORT;
|
||||||
|
let examplesRoot = null;
|
||||||
|
|
||||||
|
function exists(p) {
|
||||||
|
try {
|
||||||
|
return fs.existsSync(p);
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveExamplesRoot() {
|
||||||
|
if (process.env.BRIDGESWARM_EXAMPLES_DIR) {
|
||||||
|
return path.resolve(process.env.BRIDGESWARM_EXAMPLES_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidates = [];
|
||||||
|
if (process.env.BRIDGE_SWARM_STORAGE) {
|
||||||
|
candidates.push(path.join(path.dirname(process.env.BRIDGE_SWARM_STORAGE), 'examples'));
|
||||||
|
}
|
||||||
|
candidates.push(path.join(process.cwd(), 'examples'));
|
||||||
|
candidates.push(path.join(__dirname, 'examples'));
|
||||||
|
candidates.push(path.join(__dirname, '..', 'examples'));
|
||||||
|
|
||||||
|
for (const dir of candidates) {
|
||||||
|
if (exists(path.join(dir, 'index.html'))) return dir;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeJoin(root, urlPath) {
|
||||||
|
const decoded = decodeURIComponent((urlPath || '/').split('?')[0]);
|
||||||
|
const cleaned = path.normalize(decoded).replace(/^(\.\.[/\\])+/, '');
|
||||||
|
const full = path.join(root, cleaned);
|
||||||
|
const rootNorm = path.resolve(root);
|
||||||
|
const fullNorm = path.resolve(full);
|
||||||
|
if (fullNorm !== rootNorm && !fullNorm.startsWith(rootNorm + path.sep)) return null;
|
||||||
|
return fullNorm;
|
||||||
|
}
|
||||||
|
|
||||||
|
function contentType(filePath) {
|
||||||
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
|
return MIME[ext] || 'application/octet-stream';
|
||||||
|
}
|
||||||
|
|
||||||
|
function send(res, status, body, headers = {}) {
|
||||||
|
const buf = typeof body === 'string' ? Buffer.from(body) : body;
|
||||||
|
res.statusCode = status;
|
||||||
|
res.setHeader('Cache-Control', 'no-store');
|
||||||
|
for (const [name, value] of Object.entries(headers)) {
|
||||||
|
res.setHeader(name, value);
|
||||||
|
}
|
||||||
|
res.end(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRequest(req, res) {
|
||||||
|
const root = examplesRoot;
|
||||||
|
if (!root) {
|
||||||
|
send(res, 503, 'Examples not found on disk');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let urlPath = req.url || '/';
|
||||||
|
if (urlPath === '/') urlPath = '/index.html';
|
||||||
|
|
||||||
|
const filePath = safeJoin(root, urlPath);
|
||||||
|
if (!filePath) {
|
||||||
|
send(res, 403, 'Forbidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let st;
|
||||||
|
try {
|
||||||
|
st = fs.statSync(filePath);
|
||||||
|
} catch (_) {
|
||||||
|
send(res, 404, 'Not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = filePath;
|
||||||
|
if (st.isDirectory()) {
|
||||||
|
target = path.join(filePath, 'index.html');
|
||||||
|
if (!exists(target)) {
|
||||||
|
send(res, 404, 'Not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = fs.readFileSync(target);
|
||||||
|
send(res, 200, data, { 'Content-Type': contentType(target) });
|
||||||
|
} catch (_) {
|
||||||
|
send(res, 404, 'Not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function status() {
|
||||||
|
const root = examplesRoot || resolveExamplesRoot();
|
||||||
|
const url = server ? `http://${boundHost}:${boundPort}/` : null;
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
running: !!server,
|
||||||
|
url,
|
||||||
|
host: boundHost,
|
||||||
|
port: boundPort,
|
||||||
|
root: root || null,
|
||||||
|
rootFound: !!(root && exists(path.join(root, 'index.html'))),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function start(opts = {}) {
|
||||||
|
if (server) {
|
||||||
|
return { ...status(), ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = resolveExamplesRoot();
|
||||||
|
if (!root) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error:
|
||||||
|
'Examples directory not found. Reinstall BridgeSwarm or set BRIDGESWARM_EXAMPLES_DIR.',
|
||||||
|
running: false,
|
||||||
|
url: null,
|
||||||
|
root: null,
|
||||||
|
rootFound: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
examplesRoot = root;
|
||||||
|
boundHost = typeof opts.host === 'string' && opts.host ? opts.host : DEFAULT_HOST;
|
||||||
|
boundPort = typeof opts.port === 'number' && opts.port > 0 ? opts.port : DEFAULT_PORT;
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const s = http.createServer(handleRequest);
|
||||||
|
const onError = (err) => {
|
||||||
|
s.removeListener('listening', onListening);
|
||||||
|
server = null;
|
||||||
|
resolve({
|
||||||
|
ok: false,
|
||||||
|
error: err.message || String(err),
|
||||||
|
running: false,
|
||||||
|
url: null,
|
||||||
|
host: boundHost,
|
||||||
|
port: boundPort,
|
||||||
|
root: examplesRoot,
|
||||||
|
rootFound: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const onListening = () => {
|
||||||
|
s.removeListener('error', onError);
|
||||||
|
server = s;
|
||||||
|
resolve({ ...status(), ok: true });
|
||||||
|
};
|
||||||
|
s.once('error', onError);
|
||||||
|
s.once('listening', onListening);
|
||||||
|
s.listen(boundPort, boundHost);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop() {
|
||||||
|
if (!server) {
|
||||||
|
return Promise.resolve({ ...status(), ok: true, running: false, url: null });
|
||||||
|
}
|
||||||
|
const s = server;
|
||||||
|
server = null;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
try {
|
||||||
|
s.close(() => resolve({ ...status(), ok: true, running: false, url: null }));
|
||||||
|
} catch (_) {
|
||||||
|
resolve({ ...status(), ok: true, running: false, url: null });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { start, stop, status, resolveExamplesRoot, DEFAULT_HOST, DEFAULT_PORT };
|
||||||
@@ -19,6 +19,7 @@ const minimalDefinition = require('./hyperdb-minimal-definition.js');
|
|||||||
const b4a = require('b4a');
|
const b4a = require('b4a');
|
||||||
const capabilities = require('./capabilities/registry.js');
|
const capabilities = require('./capabilities/registry.js');
|
||||||
const { getStorageRoot } = require('./capabilities/paths.js');
|
const { getStorageRoot } = require('./capabilities/paths.js');
|
||||||
|
const examplesServer = require('./examples-server.js');
|
||||||
|
|
||||||
// File-based logging
|
// File-based logging
|
||||||
const LOG_FILE = path.join(__dirname, 'bridge-swarm.log');
|
const LOG_FILE = path.join(__dirname, 'bridge-swarm.log');
|
||||||
@@ -1183,6 +1184,19 @@ async function handleMessageAsync(send, msg) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'examplesServer.start': {
|
||||||
|
reply(await examplesServer.start(payload || {}));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'examplesServer.stop': {
|
||||||
|
reply(await examplesServer.stop());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'examplesServer.status': {
|
||||||
|
reply(examplesServer.status());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'capabilities.list': {
|
case 'capabilities.list': {
|
||||||
reply({ ok: true, packs: capabilities.listPackIds() });
|
reply({ ok: true, packs: capabilities.listPackIds() });
|
||||||
break;
|
break;
|
||||||
@@ -1264,6 +1278,9 @@ async function handleMessageAsync(send, msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cleanup() {
|
function cleanup() {
|
||||||
|
try {
|
||||||
|
examplesServer.stop().catch(() => {});
|
||||||
|
} catch (_) {}
|
||||||
try {
|
try {
|
||||||
const mediaPack = capabilities.getPack('media');
|
const mediaPack = capabilities.getPack('media');
|
||||||
if (mediaPack && typeof mediaPack.cleanup === 'function') mediaPack.cleanup();
|
if (mediaPack && typeof mediaPack.cleanup === 'function') mediaPack.cleanup();
|
||||||
|
|||||||
Generated
+1
@@ -14,6 +14,7 @@
|
|||||||
"bare-fetch": "^3.2.0",
|
"bare-fetch": "^3.2.0",
|
||||||
"bare-ffmpeg": "^1.5.0",
|
"bare-ffmpeg": "^1.5.0",
|
||||||
"bare-fs": "^4.7.4",
|
"bare-fs": "^4.7.4",
|
||||||
|
"bare-http1": "^4.5.7",
|
||||||
"bare-media": "^2.10.1",
|
"bare-media": "^2.10.1",
|
||||||
"bare-module": "^6.4.0",
|
"bare-module": "^6.4.0",
|
||||||
"bare-path": "^3.1.1",
|
"bare-path": "^3.1.1",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"bare-fetch": "^3.2.0",
|
"bare-fetch": "^3.2.0",
|
||||||
"bare-ffmpeg": "^1.5.0",
|
"bare-ffmpeg": "^1.5.0",
|
||||||
"bare-fs": "^4.7.4",
|
"bare-fs": "^4.7.4",
|
||||||
|
"bare-http1": "^4.5.7",
|
||||||
"bare-media": "^2.10.1",
|
"bare-media": "^2.10.1",
|
||||||
"bare-module": "^6.4.0",
|
"bare-module": "^6.4.0",
|
||||||
"bare-path": "^3.1.1",
|
"bare-path": "^3.1.1",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"build:hyperdb": "node scripts/build-hyperdb.js",
|
"build:hyperdb": "node scripts/build-hyperdb.js",
|
||||||
"build": "npm run build:host && npm run build:protomux && npm run build:hrpc && npm run build:hyperdb",
|
"build": "npm run build:host && npm run build:protomux && npm run build:hrpc && npm run build:hyperdb",
|
||||||
"pack": "node scripts/pack-extension.js",
|
"pack": "node scripts/pack-extension.js",
|
||||||
|
"sync:examples": "node scripts/sync-examples.js",
|
||||||
"examples": "node scripts/serve-examples.js",
|
"examples": "node scripts/serve-examples.js",
|
||||||
"repair:macos": "bash scripts/repair-macos-codesign.sh",
|
"repair:macos": "bash scripts/repair-macos-codesign.sh",
|
||||||
"build:dist": "node scripts/build-distributable.js",
|
"build:dist": "node scripts/build-distributable.js",
|
||||||
|
|||||||
@@ -280,6 +280,12 @@ async function createZipArchives(builtEntries) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const examplesDir = path.join(NATIVE_HOST_DIR, 'examples');
|
||||||
|
if (!fs.existsSync(path.join(examplesDir, 'index.html'))) {
|
||||||
|
console.log(' Syncing examples for host packages...');
|
||||||
|
execSync('node scripts/sync-examples.js', { cwd: ROOT, stdio: 'inherit' });
|
||||||
|
}
|
||||||
|
|
||||||
for (const [host, file] of byHost) {
|
for (const [host, file] of byHost) {
|
||||||
const zipName = `${HOST_NAME}-${host}.zip`;
|
const zipName = `${HOST_NAME}-${host}.zip`;
|
||||||
const zipPath = path.join(RELEASES_DIR, zipName);
|
const zipPath = path.join(RELEASES_DIR, zipName);
|
||||||
@@ -295,6 +301,9 @@ async function createZipArchives(builtEntries) {
|
|||||||
} else {
|
} else {
|
||||||
archive.file(file, { name: path.basename(file) });
|
archive.file(file, { name: path.basename(file) });
|
||||||
}
|
}
|
||||||
|
if (fs.existsSync(path.join(examplesDir, 'index.html'))) {
|
||||||
|
archive.directory(examplesDir, 'examples');
|
||||||
|
}
|
||||||
archive.finalize();
|
archive.finalize();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -311,6 +320,9 @@ async function createZipArchives(builtEntries) {
|
|||||||
async function build(hosts, doPackage) {
|
async function build(hosts, doPackage) {
|
||||||
patchBareBuildSignForLinux();
|
patchBareBuildSignForLinux();
|
||||||
|
|
||||||
|
console.log('Syncing examples into native-host...');
|
||||||
|
execSync('node scripts/sync-examples.js', { cwd: ROOT, stdio: 'inherit' });
|
||||||
|
|
||||||
// Ensure HRPC spec is mirrored into native-host before packing
|
// Ensure HRPC spec is mirrored into native-host before packing
|
||||||
console.log('Building HRPC spec...');
|
console.log('Building HRPC spec...');
|
||||||
execSync('npm run build:hrpc', { cwd: ROOT, stdio: 'inherit' });
|
execSync('npm run build:hrpc', { cwd: ROOT, stdio: 'inherit' });
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ if [[ ! -f "$EXT_DIR/manifest.json" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo " Syncing examples into extension + native-host..."
|
||||||
|
node "$REPO_ROOT/scripts/sync-examples.js"
|
||||||
|
|
||||||
if command -v pbcopy >/dev/null 2>&1; then
|
if command -v pbcopy >/dev/null 2>&1; then
|
||||||
echo "$EXT_DIR" | pbcopy
|
echo "$EXT_DIR" | pbcopy
|
||||||
elif command -v xclip >/dev/null 2>&1; then
|
elif command -v xclip >/dev/null 2>&1; then
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ else
|
|||||||
echo " node_modules exists, skipping npm install"
|
echo " node_modules exists, skipping npm install"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Syncing examples into extension and native-host..."
|
||||||
|
node "$REPO_ROOT/scripts/sync-examples.js" || true
|
||||||
|
|
||||||
# Prefer Bare bundled with native-host (npm package `bare` → bare-runtime).
|
# Prefer Bare bundled with native-host (npm package `bare` → bare-runtime).
|
||||||
BARE_PATH=""
|
BARE_PATH=""
|
||||||
LOCAL_BARE="$HOST_DIR/node_modules/bare/bin/bare"
|
LOCAL_BARE="$HOST_DIR/node_modules/bare/bin/bare"
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ $Bat = @"
|
|||||||
@echo off
|
@echo off
|
||||||
set "DIR=%~dp0"
|
set "DIR=%~dp0"
|
||||||
if not defined BRIDGE_SWARM_STORAGE set "BRIDGE_SWARM_STORAGE=%DIR%bridge-swarm-storage"
|
if not defined BRIDGE_SWARM_STORAGE set "BRIDGE_SWARM_STORAGE=%DIR%bridge-swarm-storage"
|
||||||
|
if not defined BRIDGESWARM_EXAMPLES_DIR set "BRIDGESWARM_EXAMPLES_DIR=%DIR%examples"
|
||||||
"%DIR%bridge-swarm-host.exe" %*
|
"%DIR%bridge-swarm-host.exe" %*
|
||||||
"@
|
"@
|
||||||
Set-Content -Path $Launcher -Value $Bat -Encoding ASCII
|
Set-Content -Path $Launcher -Value $Bat -Encoding ASCII
|
||||||
|
|||||||
+2
-2
@@ -184,7 +184,7 @@ if [[ "$PLATFORM" == "darwin" ]]; then
|
|||||||
printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>' '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' '<plist version="1.0"><dict><key>com.apple.security.cs.disable-library-validation</key><true/></dict></plist>' > "$ENTITLEMENTS_PLIST"
|
printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>' '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' '<plist version="1.0"><dict><key>com.apple.security.cs.disable-library-validation</key><true/></dict></plist>' > "$ENTITLEMENTS_PLIST"
|
||||||
codesign --force --sign - --entitlements "$ENTITLEMENTS_PLIST" "$HOST_BIN" 2>/dev/null || true
|
codesign --force --sign - --entitlements "$ENTITLEMENTS_PLIST" "$HOST_BIN" 2>/dev/null || true
|
||||||
|
|
||||||
printf '%s\n' '#!/bin/bash' 'DIR="$(cd "$(dirname "$0")" && pwd)"' 'export TMPDIR="${DIR}/tmp"' 'export BRIDGE_SWARM_STORAGE="${BRIDGE_SWARM_STORAGE:-${DIR}/bridge-swarm-storage}"' 'exec "${DIR}/bridge-swarm-host" "$@"' > "$LAUNCHER"
|
printf '%s\n' '#!/bin/bash' 'DIR="$(cd "$(dirname "$0")" && pwd)"' 'export TMPDIR="${DIR}/tmp"' 'export BRIDGE_SWARM_STORAGE="${BRIDGE_SWARM_STORAGE:-${DIR}/bridge-swarm-storage}"' 'export BRIDGESWARM_EXAMPLES_DIR="${BRIDGESWARM_EXAMPLES_DIR:-${DIR}/examples}"' 'exec "${DIR}/bridge-swarm-host" "$@"' > "$LAUNCHER"
|
||||||
chmod +x "$LAUNCHER"
|
chmod +x "$LAUNCHER"
|
||||||
HOST_BIN="$LAUNCHER"
|
HOST_BIN="$LAUNCHER"
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ if [[ "$PLATFORM" == "darwin" ]]; then
|
|||||||
fi
|
fi
|
||||||
echo " Signed ${SIGNED} native addons; main binary has library-validation disabled"
|
echo " Signed ${SIGNED} native addons; main binary has library-validation disabled"
|
||||||
else
|
else
|
||||||
printf '%s\n' '#!/bin/bash' 'DIR="$(cd "$(dirname "$0")" && pwd)"' 'export BRIDGE_SWARM_STORAGE="${BRIDGE_SWARM_STORAGE:-${DIR}/bridge-swarm-storage}"' 'exec "${DIR}/bridge-swarm-host" "$@"' > "$LAUNCHER"
|
printf '%s\n' '#!/bin/bash' 'DIR="$(cd "$(dirname "$0")" && pwd)"' 'export BRIDGE_SWARM_STORAGE="${BRIDGE_SWARM_STORAGE:-${DIR}/bridge-swarm-storage}"' 'export BRIDGESWARM_EXAMPLES_DIR="${BRIDGESWARM_EXAMPLES_DIR:-${DIR}/examples}"' 'exec "${DIR}/bridge-swarm-host" "$@"' > "$LAUNCHER"
|
||||||
chmod +x "$LAUNCHER"
|
chmod +x "$LAUNCHER"
|
||||||
HOST_BIN="$LAUNCHER"
|
HOST_BIN="$LAUNCHER"
|
||||||
echo "Installing native messaging manifest..."
|
echo "Installing native messaging manifest..."
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ async function main() {
|
|||||||
const version = getVersion();
|
const version = getVersion();
|
||||||
console.log('BridgeSwarm version from manifest:', version);
|
console.log('BridgeSwarm version from manifest:', version);
|
||||||
|
|
||||||
|
console.log('Syncing examples into extension...');
|
||||||
|
execSync('node scripts/sync-examples.js', { cwd: REPO_ROOT, stdio: 'inherit' });
|
||||||
|
|
||||||
console.log('Running npm run build:protomux...');
|
console.log('Running npm run build:protomux...');
|
||||||
execSync('npm run build:protomux', { cwd: REPO_ROOT, stdio: 'inherit' });
|
execSync('npm run build:protomux', { cwd: REPO_ROOT, stdio: 'inherit' });
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ ENTITLEMENTS_PLIST="$INSTALL_DIR/entitlements.plist"
|
|||||||
printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>' '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' '<plist version="1.0"><dict><key>com.apple.security.cs.disable-library-validation</key><true/></dict></plist>' > "$ENTITLEMENTS_PLIST"
|
printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>' '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' '<plist version="1.0"><dict><key>com.apple.security.cs.disable-library-validation</key><true/></dict></plist>' > "$ENTITLEMENTS_PLIST"
|
||||||
codesign --force --sign - --entitlements "$ENTITLEMENTS_PLIST" "$HOST_BIN" || true
|
codesign --force --sign - --entitlements "$ENTITLEMENTS_PLIST" "$HOST_BIN" || true
|
||||||
|
|
||||||
printf '%s\n' '#!/bin/bash' 'DIR="$(cd "$(dirname "$0")" && pwd)"' 'export TMPDIR="${DIR}/tmp"' 'export BRIDGE_SWARM_STORAGE="${BRIDGE_SWARM_STORAGE:-${DIR}/bridge-swarm-storage}"' 'exec "${DIR}/bridge-swarm-host" "$@"' > "$LAUNCHER"
|
printf '%s\n' '#!/bin/bash' 'DIR="$(cd "$(dirname "$0")" && pwd)"' 'export TMPDIR="${DIR}/tmp"' 'export BRIDGE_SWARM_STORAGE="${BRIDGE_SWARM_STORAGE:-${DIR}/bridge-swarm-storage}"' 'export BRIDGESWARM_EXAMPLES_DIR="${BRIDGESWARM_EXAMPLES_DIR:-${DIR}/examples}"' 'exec "${DIR}/bridge-swarm-host" "$@"' > "$LAUNCHER"
|
||||||
chmod +x "$LAUNCHER"
|
chmod +x "$LAUNCHER"
|
||||||
|
|
||||||
echo "Extracting addons into $INSTALL_DIR/tmp ..."
|
echo "Extracting addons into $INSTALL_DIR/tmp ..."
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
* Modern Chrome/Firefox treat each file:// URL as a unique opaque origin, which
|
* Modern Chrome/Firefox treat each file:// URL as a unique opaque origin, which
|
||||||
* breaks local HTML demos (scripts, styles, extension injection).
|
* breaks local HTML demos (scripts, styles, extension injection).
|
||||||
*
|
*
|
||||||
|
* Prefer the extension Settings toggle ("Enable examples server") which starts
|
||||||
|
* the same URL via the native host. This script is for repo/dev use without that.
|
||||||
|
*
|
||||||
* Usage: npm run examples
|
* Usage: npm run examples
|
||||||
* node scripts/serve-examples.js [port]
|
* node scripts/serve-examples.js [port]
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Copy repo examples/ into the extension (and optionally native-host) so they
|
||||||
|
* ship with packed builds. Source of truth remains examples/ at the repo root.
|
||||||
|
*
|
||||||
|
* Usage: node scripts/sync-examples.js
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||||
|
const SRC = path.join(REPO_ROOT, 'examples');
|
||||||
|
const DESTINATIONS = [
|
||||||
|
path.join(REPO_ROOT, 'extension', 'examples'),
|
||||||
|
path.join(REPO_ROOT, 'native-host', 'examples'),
|
||||||
|
];
|
||||||
|
|
||||||
|
function rmrf(dir) {
|
||||||
|
if (!fs.existsSync(dir)) return;
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyDir(src, dest) {
|
||||||
|
fs.mkdirSync(dest, { recursive: true });
|
||||||
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||||
|
if (entry.name === '.DS_Store') continue;
|
||||||
|
const from = path.join(src, entry.name);
|
||||||
|
const to = path.join(dest, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
copyDir(from, to);
|
||||||
|
} else {
|
||||||
|
fs.copyFileSync(from, to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
if (!fs.existsSync(path.join(SRC, 'index.html'))) {
|
||||||
|
throw new Error('examples/index.html missing — nothing to sync');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dest of DESTINATIONS) {
|
||||||
|
rmrf(dest);
|
||||||
|
copyDir(SRC, dest);
|
||||||
|
console.log('Synced examples →', path.relative(REPO_ROOT, dest));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
Reference in New Issue
Block a user