rename: the desktop is Inkwell — crates, Tauri identity, data move, packaging

Step 3 of milestone 481 (Scribe note 5071: a full rename).

- crates thoughtsync-{core,desktop,ffi,uniffi-bindgen} → inkwell-*, the
  Cargo.lock entries moved to match (checked with `cargo metadata --locked`)
- Tauri: productName "Inkwell", identifier com.fabledsword.inkwell, binary
  `inkwell`, updater feed on bvandeusen/inkwell, store file inkwell.db
- client agent inkwell-desktop, headers X-Inkwell-Client/-Protocol (the server
  reads neither), capture event inkwell://captured, display-version env
- .deb: conflicts + replaces thought-sync, so the updater's install retires the
  old package instead of colliding on it. kebab-case("Inkwell") is `inkwell`, so
  the package name finally matches the command and verify.sh now asserts it
- pacman: inkwell, conflicting with and replacing thoughtsync and
  thoughtsync-desktop
- AppImage ~/Applications/Inkwell.AppImage, menu entry inkwell.desktop,
  installer, release titles, desktop asset names in fetch-clients.sh

The one shim, chosen by the operator because it is the only copy of a
local-first user's notes: crossover.rs moves the old
com.fabledsword.thoughtsync app-data dir's contents into the new one on startup,
before the store opens, renaming thoughtsync.db and its -wal/-shm with it. It
skips when the new dir already has a store, and anything already in the new dir
wins (the installer writes its channel marker there first). Tested.

Android's Kotlin side (package, applicationId, uniffi class) is step 4. Its
release asset names stay thoughtsync.* until then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 14:27:26 -04:00
co-authored by Claude Opus 5.5
parent a706644455
commit fe6f0746b2
37 changed files with 453 additions and 247 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "thoughtsync-core"
name = "inkwell-core"
version = "0.1.0"
description = "ThoughtSync client core — local-first SQLite store and opt-in sync engine"
description = "Inkwell client core — local-first SQLite store and opt-in sync engine"
authors = ["bvandeusen"]
edition = "2021"
+1 -1
View File
@@ -1,4 +1,4 @@
//! ThoughtSync's client core: the on-device SQLite store and the sync engine.
//! Inkwell's client core: the on-device SQLite store and the sync engine.
//!
//! Deliberately free of any UI framework. The desktop wraps it in Tauri commands;
//! the Android client binds it through uniffi. Neither owns it, and a change to
+2 -2
View File
@@ -427,8 +427,8 @@ mod tests {
#[test]
fn urls_preserve_a_port_and_subpath() {
assert_eq!(
config_url("http://192.168.1.10:8000/thoughtsync"),
"http://192.168.1.10:8000/thoughtsync/api/config"
config_url("http://192.168.1.10:8000/inkwell"),
"http://192.168.1.10:8000/inkwell/api/config"
);
}
}
+11 -16
View File
@@ -176,7 +176,7 @@ pub fn evaluate(info: &ServerInfo) -> Compatibility {
/// Who this client says it is, set once by the host application at startup.
///
/// THE CORE CANNOT KNOW THIS, and the value it used to invent was wrong twice. It
/// was `thoughtsync-desktop/{CARGO_PKG_VERSION}`, and this crate is compiled into
/// was `inkwell-desktop/{CARGO_PKG_VERSION}`, and this crate is compiled into
/// the desktop app AND the Android app — so every phone in the field announced
/// itself as a desktop. The version was worse: `CARGO_PKG_VERSION` here is the
/// version of the CORE crate, a number no build stamps and no user has ever seen,
@@ -188,7 +188,7 @@ pub fn evaluate(info: &ServerInfo) -> Compatibility {
/// the process and a second caller should be ignored rather than race the first.
static CLIENT_AGENT: OnceLock<String> = OnceLock::new();
/// Name this client for the servers it talks to — `("thoughtsync-android", "2026.08.31.1204")`.
/// Name this client for the servers it talks to — `("inkwell-android", "2026.08.31.1204")`.
///
/// Call once at startup, before any sync. Calling twice is not an error and the
/// first name wins; not calling it at all is visible in the header rather than
@@ -207,13 +207,10 @@ pub fn client_headers() -> [(&'static str, String); 2] {
let agent = CLIENT_AGENT
.get()
.cloned()
.unwrap_or_else(|| "thoughtsync-unidentified/unknown".to_string());
.unwrap_or_else(|| "inkwell-unidentified/unknown".to_string());
[
("X-ThoughtSync-Client", agent),
(
"X-ThoughtSync-Protocol",
CLIENT_PROTOCOL_VERSION.to_string(),
),
("X-Inkwell-Client", agent),
("X-Inkwell-Protocol", CLIENT_PROTOCOL_VERSION.to_string()),
]
}
@@ -411,21 +408,19 @@ mod tests {
// this one for the OnceLock, and whichever lost would see the other's name.
// One test, both branches, in order.
assert!(
client_headers()[0]
.1
.starts_with("thoughtsync-unidentified/"),
client_headers()[0].1.starts_with("inkwell-unidentified/"),
"a host that never introduced itself must say so"
);
set_client_agent("thoughtsync-test", "2026.08.31.1204");
set_client_agent("inkwell-test", "2026.08.31.1204");
let headers = client_headers();
assert_eq!(headers[0].0, "X-ThoughtSync-Client");
assert_eq!(headers[0].1, "thoughtsync-test/2026.08.31.1204");
assert_eq!(headers[0].0, "X-Inkwell-Client");
assert_eq!(headers[0].1, "inkwell-test/2026.08.31.1204");
assert_eq!(headers[1].1, CLIENT_PROTOCOL_VERSION.to_string());
// First name wins — a second host cannot rename a running process.
set_client_agent("thoughtsync-impostor", "0");
assert_eq!(client_headers()[0].1, "thoughtsync-test/2026.08.31.1204");
set_client_agent("inkwell-impostor", "0");
assert_eq!(client_headers()[0].1, "inkwell-test/2026.08.31.1204");
}
#[test]
+1 -1
View File
@@ -1,4 +1,4 @@
//! Talking to a ThoughtSync server — entirely opt-in.
//! Talking to an Inkwell server — entirely opt-in.
//!
//! The app is local-first: `local` is the source of truth and everything works
//! unlinked. Nothing in here runs until the user links a server.