ruby_everywhere 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/everywhere/javascript/bridge.js +46 -2
- data/lib/everywhere/version.rb +2 -2
- data/support/shell/src-tauri/src/main.rs +10 -0
- data/support/shell/src-tauri/src/updater.rs +106 -11
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b0f0efd687867d44e9b4223bd1bcdda51dd543236d8510069d9232b81e5e31dd
|
|
4
|
+
data.tar.gz: 4070286991240decb7b38abc1766a360e996153d8640f8a1f1c6339f247de6d9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a5ba33fca0834b228b5567695414896cc218adc5d082a2ea46c33f728148f8b9f2fbfc09ab0455a5473f8f6a327352f9e38b489364092fe7f842e9e6a395a244
|
|
7
|
+
data.tar.gz: e6067ab08a817595a5076be7bdbd8dbb7a3a06964bbc5682409e4ad2a2cd7c1d7e87b07a4e6bd93e0ac48964553b70f0f3dbfd6fb693da53ccde51a7e3b5e3ae
|
|
@@ -17,9 +17,11 @@
|
|
|
17
17
|
// Everywhere.visit("/settings") // Turbo.visit with location fallback
|
|
18
18
|
//
|
|
19
19
|
// Everywhere.updates.supported // true when the shell has an update feed
|
|
20
|
+
// Everywhere.updates.channel // effective channel ("stable", "beta", …)
|
|
20
21
|
// Everywhere.updates.check() // Promise<{available, version?, notes?, notesHtml?}>
|
|
21
22
|
// Everywhere.updates.install() // download/verify/swap/relaunch
|
|
22
|
-
// Everywhere.updates.
|
|
23
|
+
// Everywhere.updates.setChannel("beta")// Promise<{channel}>, persisted by the shell
|
|
24
|
+
// Everywhere.updates.on("available" | "none" | "progress" | "ready" | "error" | "channel", handler)
|
|
23
25
|
//
|
|
24
26
|
// notes is the markdown source; notesHtml is the same notes pre-rendered to
|
|
25
27
|
// HTML (from the signed update feed — your own content), ready for a
|
|
@@ -110,6 +112,25 @@ const desktop = {
|
|
|
110
112
|
|
|
111
113
|
updatesInstall() {
|
|
112
114
|
return window.__TAURI__.event.emit("everywhere:update-install", {})
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
// Ask the shell to switch update channels. The shell validates, persists the
|
|
118
|
+
// choice (it survives relaunches and overrides everywhere.yml), and always
|
|
119
|
+
// answers: update-channel on success, update-error on rejection.
|
|
120
|
+
updatesSetChannel(channel) {
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
const offs = []
|
|
123
|
+
let timer = null
|
|
124
|
+
const settle = (fn) => (payload) => {
|
|
125
|
+
offs.forEach((off) => off())
|
|
126
|
+
clearTimeout(timer)
|
|
127
|
+
fn(payload)
|
|
128
|
+
}
|
|
129
|
+
offs.push(this.on("update-channel", settle((p) => resolve({ channel: p.channel }))))
|
|
130
|
+
offs.push(this.on("update-error", settle((p) => reject(new Error(p.message)))))
|
|
131
|
+
timer = setTimeout(settle(() => reject(new Error("channel change timed out"))), 10000)
|
|
132
|
+
window.__TAURI__.event.emit("everywhere:update-set-channel", { channel })
|
|
133
|
+
})
|
|
113
134
|
}
|
|
114
135
|
}
|
|
115
136
|
|
|
@@ -160,6 +181,11 @@ const browser = {
|
|
|
160
181
|
updatesInstall() {
|
|
161
182
|
console.log("[everywhere] updates unavailable in the browser")
|
|
162
183
|
return Promise.resolve()
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
updatesSetChannel() {
|
|
187
|
+
console.log("[everywhere] update channel unavailable in the browser")
|
|
188
|
+
return Promise.resolve({ channel: null, unsupported: true })
|
|
163
189
|
}
|
|
164
190
|
}
|
|
165
191
|
|
|
@@ -182,6 +208,9 @@ const platform = detectPlatform()
|
|
|
182
208
|
const os = detectOS()
|
|
183
209
|
const adapter = adapters[platform]
|
|
184
210
|
|
|
211
|
+
// Mutable so setChannel can keep updates.channel truthful without a reload.
|
|
212
|
+
let updatesChannel = (config.updates && config.updates.channel) || null
|
|
213
|
+
|
|
185
214
|
export const Everywhere = {
|
|
186
215
|
platform,
|
|
187
216
|
os,
|
|
@@ -228,7 +257,13 @@ export const Everywhere = {
|
|
|
228
257
|
return platform === "desktop" && !!config.updates
|
|
229
258
|
},
|
|
230
259
|
version: config.version || null,
|
|
231
|
-
|
|
260
|
+
|
|
261
|
+
// The channel the shell is actually checking. The shell injects the
|
|
262
|
+
// effective value (a persisted user choice overrides everywhere.yml), and
|
|
263
|
+
// setChannel keeps it current without a reload.
|
|
264
|
+
get channel() {
|
|
265
|
+
return updatesChannel
|
|
266
|
+
},
|
|
232
267
|
|
|
233
268
|
check() {
|
|
234
269
|
return adapter.updatesCheck()
|
|
@@ -240,6 +275,15 @@ export const Everywhere = {
|
|
|
240
275
|
return adapter.updatesInstall()
|
|
241
276
|
},
|
|
242
277
|
|
|
278
|
+
// Switch the update feed channel (e.g. "stable" -> "beta"). Resolves once
|
|
279
|
+
// the shell has persisted it; follow with check() to scan the new channel.
|
|
280
|
+
setChannel(channel) {
|
|
281
|
+
return adapter.updatesSetChannel(channel).then((result) => {
|
|
282
|
+
if (result && result.channel) updatesChannel = result.channel
|
|
283
|
+
return result
|
|
284
|
+
})
|
|
285
|
+
},
|
|
286
|
+
|
|
243
287
|
on(event, handler) {
|
|
244
288
|
return adapter.on(`update-${event}`, handler)
|
|
245
289
|
}
|
data/lib/everywhere/version.rb
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Everywhere
|
|
4
|
-
VERSION = "0.1.
|
|
4
|
+
VERSION = "0.1.9"
|
|
5
5
|
|
|
6
6
|
# Version of the vendored @rubyeverywhere/bridge JS this gem ships. Tracks
|
|
7
7
|
# bridge/package.json — bump it whenever lib/everywhere/javascript/bridge.js is
|
|
8
8
|
# refreshed. `every install` stamps it into the vendored file so the build
|
|
9
9
|
# receipt can record which bridge shipped; it versions independently of the CLI.
|
|
10
|
-
BRIDGE_VERSION = "0.
|
|
10
|
+
BRIDGE_VERSION = "0.3.0"
|
|
11
11
|
end
|
|
@@ -143,6 +143,16 @@ fn main() {
|
|
|
143
143
|
.plugin(tauri_plugin_clipboard_manager::init())
|
|
144
144
|
.invoke_handler(tauri::generate_handler![notify_command])
|
|
145
145
|
.setup(move |app| {
|
|
146
|
+
// A persisted user channel choice (Everywhere.updates.setChannel)
|
|
147
|
+
// overrides everywhere.yml's updates.channel. Fold it in before
|
|
148
|
+
// anything reads raw_json — the updater, the menu, and the
|
|
149
|
+
// __EVERYWHERE_CONFIG__ page injection all see the same channel.
|
|
150
|
+
let mut config = config;
|
|
151
|
+
if let Ok(mut value) = serde_json::from_str::<serde_json::Value>(&config.raw_json) {
|
|
152
|
+
updater::apply_channel_override(app.handle(), &mut value);
|
|
153
|
+
config.raw_json = value.to_string();
|
|
154
|
+
}
|
|
155
|
+
|
|
146
156
|
// Bridge channel: pages emit events (the sanctioned webview->shell
|
|
147
157
|
// path for remote-origin content in Tauri v2; invoke is reserved
|
|
148
158
|
// for local content). Fire-and-forget, Strada-style.
|
|
@@ -10,10 +10,16 @@
|
|
|
10
10
|
// codesign --verify --deep --strict -> Info.plist bundle_id + version match.
|
|
11
11
|
//
|
|
12
12
|
// Page JS drives it through bridge events (everywhere:update-check /
|
|
13
|
-
// update-install in; update-available / -none /
|
|
14
|
-
// out); the "Check for Updates…" menu
|
|
15
|
-
// dialogs. `updates.auto` picks the
|
|
16
|
-
// download | install (silent swap staged
|
|
13
|
+
// update-install / update-set-channel in; update-available / -none /
|
|
14
|
+
// -progress / -ready / -error / -channel out); the "Check for Updates…" menu
|
|
15
|
+
// item drives the same flows with native dialogs. `updates.auto` picks the
|
|
16
|
+
// background behaviour: off | check | download | install (silent swap staged
|
|
17
|
+
// in RAM-free renames on quit).
|
|
18
|
+
//
|
|
19
|
+
// Channel: everywhere.yml's updates.channel is the default; a user choice made
|
|
20
|
+
// through the bridge (Everywhere.updates.setChannel) is persisted to the app
|
|
21
|
+
// data dir and overrides it from then on — the server-driven settings page
|
|
22
|
+
// decides who gets to see a "beta" option, the shell just honors the pick.
|
|
17
23
|
|
|
18
24
|
use std::path::{Path, PathBuf};
|
|
19
25
|
use std::process::Command;
|
|
@@ -61,10 +67,20 @@ struct Pending {
|
|
|
61
67
|
|
|
62
68
|
pub struct UpdaterState {
|
|
63
69
|
cfg: UpdateConfig,
|
|
70
|
+
// The live channel. Starts as cfg.channel (which already reflects a
|
|
71
|
+
// persisted override — see apply_channel_override) and moves when the page
|
|
72
|
+
// calls setChannel.
|
|
73
|
+
channel: Mutex<String>,
|
|
64
74
|
pending: Mutex<Option<Pending>>,
|
|
65
75
|
busy: AtomicBool,
|
|
66
76
|
}
|
|
67
77
|
|
|
78
|
+
impl UpdaterState {
|
|
79
|
+
fn channel(&self) -> String {
|
|
80
|
+
self.channel.lock().unwrap().clone()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
68
84
|
impl UpdateConfig {
|
|
69
85
|
// The shell subset of everywhere.yml's updates: section, from the same
|
|
70
86
|
// raw config JSON the rest of the shell reads. None => updater disabled.
|
|
@@ -107,13 +123,31 @@ pub fn enabled(raw_json: &str) -> bool {
|
|
|
107
123
|
UpdateConfig::parse(raw_json).is_some()
|
|
108
124
|
}
|
|
109
125
|
|
|
126
|
+
// Fold the persisted user channel choice (if any) into the raw config value.
|
|
127
|
+
// main.rs calls this before the config is parsed OR injected into pages, so
|
|
128
|
+
// the updater, the menu, and __EVERYWHERE_CONFIG__.updates.channel all agree
|
|
129
|
+
// on the effective channel.
|
|
130
|
+
pub fn apply_channel_override(handle: &tauri::AppHandle, value: &mut serde_json::Value) {
|
|
131
|
+
if !value["updates"].is_object() {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
let bundle_id = value["bundle_id"].as_str().unwrap_or("com.rubyeverywhere.app").to_string();
|
|
135
|
+
let Some(path) = channel_file(handle, &bundle_id) else { return };
|
|
136
|
+
let Ok(saved) = std::fs::read_to_string(&path) else { return };
|
|
137
|
+
let saved = saved.trim();
|
|
138
|
+
if valid_channel(saved) {
|
|
139
|
+
value["updates"]["channel"] = serde_json::json!(saved);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
110
143
|
pub fn init(app: &tauri::App, raw_json: &str) {
|
|
111
144
|
let Some(cfg) = UpdateConfig::parse(raw_json) else {
|
|
112
145
|
return;
|
|
113
146
|
};
|
|
114
147
|
let auto = cfg.auto;
|
|
115
148
|
let interval = cfg.interval;
|
|
116
|
-
|
|
149
|
+
let channel = Mutex::new(cfg.channel.clone());
|
|
150
|
+
app.manage(UpdaterState { cfg, channel, pending: Mutex::new(None), busy: AtomicBool::new(false) });
|
|
117
151
|
|
|
118
152
|
{
|
|
119
153
|
let handle = app.handle().clone();
|
|
@@ -140,6 +174,14 @@ pub fn init(app: &tauri::App, raw_json: &str) {
|
|
|
140
174
|
});
|
|
141
175
|
});
|
|
142
176
|
}
|
|
177
|
+
{
|
|
178
|
+
let handle = app.handle().clone();
|
|
179
|
+
app.listen_any("everywhere:update-set-channel", move |event| {
|
|
180
|
+
let payload = event.payload().to_string();
|
|
181
|
+
let handle = handle.clone();
|
|
182
|
+
std::thread::spawn(move || set_channel(&handle, &payload));
|
|
183
|
+
});
|
|
184
|
+
}
|
|
143
185
|
|
|
144
186
|
if auto != Auto::Off {
|
|
145
187
|
let handle = app.handle().clone();
|
|
@@ -156,13 +198,53 @@ pub fn init(app: &tauri::App, raw_json: &str) {
|
|
|
156
198
|
|
|
157
199
|
// ---- flows ------------------------------------------------------------------
|
|
158
200
|
|
|
201
|
+
// Bridge-triggered channel switch (Everywhere.updates.setChannel). Validates,
|
|
202
|
+
// persists the choice so it survives relaunches, retargets the running
|
|
203
|
+
// updater, and always answers: update-channel on success, update-error on a
|
|
204
|
+
// bad request. A staged download from the old channel is discarded — it no
|
|
205
|
+
// longer represents what the user asked to run.
|
|
206
|
+
fn set_channel(handle: &tauri::AppHandle, payload: &str) {
|
|
207
|
+
let Some(state) = state(handle) else { return };
|
|
208
|
+
let requested = serde_json::from_str::<serde_json::Value>(payload)
|
|
209
|
+
.ok()
|
|
210
|
+
.and_then(|v| v["channel"].as_str().map(str::to_string));
|
|
211
|
+
let Some(channel) = requested.filter(|c| valid_channel(c)) else {
|
|
212
|
+
emit_error(handle, "invalid update channel (letters, digits, . _ - only)");
|
|
213
|
+
return;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
if let Some(path) = channel_file(handle, &state.cfg.bundle_id) {
|
|
217
|
+
let write = path
|
|
218
|
+
.parent()
|
|
219
|
+
.map(|dir| std::fs::create_dir_all(dir).map_err(|e| e.to_string()))
|
|
220
|
+
.unwrap_or(Ok(()))
|
|
221
|
+
.and_then(|()| std::fs::write(&path, &channel).map_err(|e| e.to_string()));
|
|
222
|
+
if let Err(e) = write {
|
|
223
|
+
emit_error(handle, &format!("could not save channel choice: {e}"));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let changed = {
|
|
229
|
+
let mut current = state.channel.lock().unwrap();
|
|
230
|
+
let changed = *current != channel;
|
|
231
|
+
*current = channel.clone();
|
|
232
|
+
changed
|
|
233
|
+
};
|
|
234
|
+
if changed {
|
|
235
|
+
*state.pending.lock().unwrap() = None;
|
|
236
|
+
}
|
|
237
|
+
println!("[updater] channel set to {channel}");
|
|
238
|
+
let _ = handle.emit_to("main", "everywhere:update-channel", serde_json::json!({ "channel": channel }));
|
|
239
|
+
}
|
|
240
|
+
|
|
159
241
|
// Bridge-triggered check: always answers with an event (available/none/error).
|
|
160
242
|
fn explicit_check(handle: &tauri::AppHandle) {
|
|
161
243
|
let Some(state) = state(handle) else { return };
|
|
162
244
|
if state.busy.swap(true, Ordering::SeqCst) {
|
|
163
245
|
return;
|
|
164
246
|
}
|
|
165
|
-
match check(&state.cfg) {
|
|
247
|
+
match check(&state.cfg, &state.channel()) {
|
|
166
248
|
Ok(Some(m)) => emit_available(handle, &m),
|
|
167
249
|
Ok(None) => {
|
|
168
250
|
let _ = handle.emit_to("main", "everywhere:update-none",
|
|
@@ -181,7 +263,7 @@ pub fn interactive_check(handle: &tauri::AppHandle) {
|
|
|
181
263
|
if state.busy.swap(true, Ordering::SeqCst) {
|
|
182
264
|
return;
|
|
183
265
|
}
|
|
184
|
-
let outcome = check(&state.cfg);
|
|
266
|
+
let outcome = check(&state.cfg, &state.channel());
|
|
185
267
|
state.busy.store(false, Ordering::SeqCst);
|
|
186
268
|
|
|
187
269
|
match outcome {
|
|
@@ -225,7 +307,7 @@ fn background_pass(handle: &tauri::AppHandle, auto: Auto) {
|
|
|
225
307
|
return;
|
|
226
308
|
}
|
|
227
309
|
let result = (|| -> Result<(), String> {
|
|
228
|
-
let Some(m) = check(&state.cfg)? else { return Ok(()) };
|
|
310
|
+
let Some(m) = check(&state.cfg, &state.channel())? else { return Ok(()) };
|
|
229
311
|
emit_available(handle, &m);
|
|
230
312
|
if auto >= Auto::Download && !is_staged(&state, &m) {
|
|
231
313
|
let staged = download_and_stage(handle, &state.cfg, &m)?;
|
|
@@ -252,7 +334,7 @@ fn install_flow(handle: &tauri::AppHandle) -> Result<(), String> {
|
|
|
252
334
|
let pending = match pending {
|
|
253
335
|
Some(p) => p,
|
|
254
336
|
None => {
|
|
255
|
-
let m = check(&state.cfg)?.ok_or("already up to date")?;
|
|
337
|
+
let m = check(&state.cfg, &state.channel())?.ok_or("already up to date")?;
|
|
256
338
|
emit_available(handle, &m);
|
|
257
339
|
let staged = download_and_stage(handle, &state.cfg, &m)?;
|
|
258
340
|
Pending { manifest: m, extracted_app: staged }
|
|
@@ -284,10 +366,10 @@ pub fn install_pending_on_exit(app: &tauri::AppHandle) {
|
|
|
284
366
|
|
|
285
367
|
// ---- check ------------------------------------------------------------------
|
|
286
368
|
|
|
287
|
-
fn check(cfg: &UpdateConfig) -> Result<Option<Manifest>, String> {
|
|
369
|
+
fn check(cfg: &UpdateConfig, channel: &str) -> Result<Option<Manifest>, String> {
|
|
288
370
|
let url = format!(
|
|
289
371
|
"{}/{}/{}/{}/latest.json?current={}",
|
|
290
|
-
cfg.url,
|
|
372
|
+
cfg.url, channel, os_name(), arch_name(), cfg.current
|
|
291
373
|
);
|
|
292
374
|
println!("[updater] checking {url}");
|
|
293
375
|
|
|
@@ -580,6 +662,19 @@ fn state(handle: &tauri::AppHandle) -> Option<tauri::State<'_, UpdaterState>> {
|
|
|
580
662
|
handle.try_state::<UpdaterState>()
|
|
581
663
|
}
|
|
582
664
|
|
|
665
|
+
// Where the user's channel choice lives: <data dir>/<bundle_id>/update-channel,
|
|
666
|
+
// a plain-text sibling of the updates/ staging dir.
|
|
667
|
+
fn channel_file(handle: &tauri::AppHandle, bundle_id: &str) -> Option<PathBuf> {
|
|
668
|
+
handle.path().data_dir().ok().map(|d| d.join(bundle_id).join("update-channel"))
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Channel names become a path segment of the feed URL — keep them boring.
|
|
672
|
+
fn valid_channel(s: &str) -> bool {
|
|
673
|
+
!s.is_empty()
|
|
674
|
+
&& s.len() <= 64
|
|
675
|
+
&& s.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))
|
|
676
|
+
}
|
|
677
|
+
|
|
583
678
|
// notes = markdown source (also what the native dialog shows); notes_html =
|
|
584
679
|
// pre-rendered HTML for in-app changelog UI. Both ride to the page untouched —
|
|
585
680
|
// the feed is the developer's own signed content.
|