diff --git a/.env.example b/.env.example index 400b54d..eea4dd5 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -# ThoughtSync production settings. Copy to `.env` and edit: +# Inkwell production settings. Copy to `.env` and edit: # # cp .env.example .env # @@ -29,15 +29,15 @@ POSTGRES_PASSWORD= # # NOTE: `main` can sit well behind `dev`. If a feature you expect is missing, # check which branch it actually landed on before assuming a bug. -#THOUGHTSYNC_TAG=latest +#INKWELL_TAG=latest # The host port the app is published on. -#THOUGHTSYNC_PORT=5000 +#INKWELL_PORT=5000 # Which interface to bind. The default (all interfaces) is what lets desktop # clients on your network reach the server. Behind a reverse proxy, set this to # 127.0.0.1 so only the proxy can talk to it. -#THOUGHTSYNC_BIND=0.0.0.0 +#INKWELL_BIND=0.0.0.0 # NOTE: how many proxies sit in front of this app is a SETTING, not an env var — # Settings → Security → "Trusted proxy hops" in the admin UI. It defaults to 1 (one @@ -47,12 +47,31 @@ POSTGRES_PASSWORD= # How much the app says. Credential events (sign-ins, failures, throttles, new # accounts, device tokens issued) are logged at INFO and read with # `docker compose logs app`. -#THOUGHTSYNC_LOG_LEVEL=INFO +#INKWELL_LOG_LEVEL=INFO # Database identity. Changing these AFTER the first start does not rename anything # that already exists — the volume keeps whatever the first run created. +#POSTGRES_USER=inkwell +#POSTGRES_DB=inkwell + +# --- Upgrading from ThoughtSync --------------------------------------------- +# +# Inkwell was called ThoughtSync, and a deployment installed under that name has +# its data in volumes and a database named for it. Point at them instead of +# renaming anything. Leaving these unset on such a deployment starts Inkwell +# against EMPTY volumes; the old data is untouched, but you would not see it. +# +# Use the full names `docker volume ls` prints — compose prefixes them with the +# project name, so they usually look like `thoughtsync_thoughtsync-db`: +#INKWELL_DB_VOLUME=thoughtsync_thoughtsync-db +#INKWELL_DATA_VOLUME=thoughtsync_thoughtsync-data +# +# And the database identity the first start created, which a volume keeps: #POSTGRES_USER=thoughtsync #POSTGRES_DB=thoughtsync +# +# Rename any THOUGHTSYNC_* lines already in your .env to INKWELL_* — the old +# names are no longer read. # --- a note on HTTPS -------------------------------------------------------- # diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index f77daea..ec091b5 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -54,7 +54,7 @@ permissions: env: REGISTRY: git.fabledsword.com - IMAGE: git.fabledsword.com/bvandeusen/thoughtsync + IMAGE: git.fabledsword.com/bvandeusen/inkwell jobs: # Should this push build an image now, or is the Android lane about to publish a @@ -215,11 +215,11 @@ jobs: # Postgres it will actually meet. image: postgres:16-alpine env: - POSTGRES_USER: thoughtsync + POSTGRES_USER: inkwell POSTGRES_PASSWORD: ci_integration - POSTGRES_DB: thoughtsync_test + POSTGRES_DB: inkwell_test options: >- - --health-cmd "pg_isready -U thoughtsync" + --health-cmd "pg_isready -U inkwell" --health-interval 10s --health-timeout 5s --health-retries 10 @@ -243,7 +243,7 @@ jobs: test -n "$PG" PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$PG") test -n "$PG_IP" - export THOUGHTSYNC_DATABASE_URL="postgresql+asyncpg://thoughtsync:ci_integration@${PG_IP}:5432/thoughtsync_test" + export INKWELL_DATABASE_URL="postgresql+asyncpg://inkwell:ci_integration@${PG_IP}:5432/inkwell_test" # Wait for Postgres to accept connections. `run:` is busybox sh (rule 81) — # no bash /dev/tcp — so use the Python that is always present here. /opt/venv/bin/python - "$PG_IP" <<'PY' diff --git a/Dockerfile b/Dockerfile index d8106e3..6264949 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \ # Bake the built SPA into the package's static dir (served by app.py). PYTHONPATH # points at /app/src so the runtime imports this source tree (with static/ present), # not the pip-installed copy. -COPY --from=build-frontend /build/dist/ src/thoughtsync/static/ +COPY --from=build-frontend /build/dist/ src/inkwell/static/ COPY alembic.ini . COPY alembic/ alembic/ @@ -41,7 +41,7 @@ COPY alembic/ alembic/ # that step never ran. An image with no clients — or with some and not others — is # a supported state: the server advertises what it has and the web UI hides the # rest (client_dist.py). -COPY client/ src/thoughtsync/client/ +COPY client/ src/inkwell/client/ ENV PYTHONPATH=/app/src @@ -52,4 +52,4 @@ EXPOSE 5000 # Wait for the database, run migrations, then serve. The DB wait keeps a briefly # slow/unready database from crash-looping the container. Family convention # (rule 82): schema is built by real migrations, never metadata.create_all. -CMD ["sh", "-c", "python -m thoughtsync.dbwait && alembic upgrade head && hypercorn 'thoughtsync.app:create_app()' --bind 0.0.0.0:5000 --keep-alive 600"] +CMD ["sh", "-c", "python -m inkwell.dbwait && alembic upgrade head && hypercorn 'inkwell.app:create_app()' --bind 0.0.0.0:5000 --keep-alive 600"] diff --git a/README.md b/README.md index 60b5bcb..60ad5a2 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ under a second, designed to grow into a lightweight second brain (labels, search ## Layout ``` -src/thoughtsync/ Quart app (app factory, auth, models, ACL, config, db) +src/inkwell/ Quart app (app factory, auth, models, ACL, config, db) alembic/ async migrations (schema built via `alembic upgrade head`) tests/ DB-free unit tests (pytest) frontend/ Vue 3 + Vite + TypeScript + Tailwind SPA @@ -28,7 +28,7 @@ Backend (needs a Postgres reachable at `THOUGHTSYNC_DATABASE_URL`): ```sh pip install -e ".[dev]" alembic upgrade head -hypercorn 'thoughtsync.app:create_app()' --bind 0.0.0.0:5000 +hypercorn 'inkwell.app:create_app()' --bind 0.0.0.0:5000 ``` Frontend (proxies `/api` to `:5000`): diff --git a/alembic.ini b/alembic.ini index 1741e86..f1f261c 100644 --- a/alembic.ini +++ b/alembic.ini @@ -5,8 +5,8 @@ script_location = %(here)s/alembic prepend_sys_path = . src path_separator = os -# Local-dev default; overridden at runtime by THOUGHTSYNC_DATABASE_URL (see env.py). -sqlalchemy.url = postgresql+asyncpg://thoughtsync:thoughtsync@localhost:5432/thoughtsync +# Local-dev default; overridden at runtime by INKWELL_DATABASE_URL (see env.py). +sqlalchemy.url = postgresql+asyncpg://inkwell:inkwell@localhost:5432/inkwell [loggers] diff --git a/alembic/env.py b/alembic/env.py index bcfe7d2..79a3539 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -8,8 +8,8 @@ from sqlalchemy.ext.asyncio import async_engine_from_config from alembic import context -from thoughtsync.models import Base -import thoughtsync.models.all # noqa: F401 — registers every model on Base.metadata +from inkwell.models import Base +import inkwell.models.all # noqa: F401 — registers every model on Base.metadata config = context.config @@ -18,7 +18,7 @@ if config.config_file_name is not None: config.set_main_option( "sqlalchemy.url", - os.environ.get("THOUGHTSYNC_DATABASE_URL", config.get_main_option("sqlalchemy.url")), + os.environ.get("INKWELL_DATABASE_URL", config.get_main_option("sqlalchemy.url")), ) target_metadata = Base.metadata diff --git a/core/src/local/recur.rs b/core/src/local/recur.rs index 8d76bf9..a5fe542 100644 --- a/core/src/local/recur.rs +++ b/core/src/local/recur.rs @@ -1,6 +1,6 @@ //! Recurring-reminder math: where a reminder goes when it is marked done. //! -//! A deliberate port of the server's `src/thoughtsync/notes/recurrence.py`, kept +//! A deliberate port of the server's `src/inkwell/notes/recurrence.py`, kept //! behaviourally identical rather than merely similar. The same note can be //! completed from the web (server code) or from the desktop and Android (this //! code), and the two must land on the same instant — otherwise completing a diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index e788cba..23b2759 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -14,15 +14,15 @@ services: db: image: postgres:16-alpine environment: - POSTGRES_USER: thoughtsync - POSTGRES_PASSWORD: thoughtsync - POSTGRES_DB: thoughtsync + POSTGRES_USER: inkwell + POSTGRES_PASSWORD: inkwell + POSTGRES_DB: inkwell volumes: - - thoughtsync-dev-db:/var/lib/postgresql/data + - inkwell-dev-db:/var/lib/postgresql/data ports: - "5432:5432" healthcheck: - test: ["CMD-SHELL", "pg_isready -U thoughtsync"] + test: ["CMD-SHELL", "pg_isready -U inkwell"] interval: 5s timeout: 5s retries: 10 @@ -34,22 +34,22 @@ services: db: condition: service_healthy environment: - THOUGHTSYNC_DATABASE_URL: postgresql+asyncpg://thoughtsync:thoughtsync@db:5432/thoughtsync + INKWELL_DATABASE_URL: postgresql+asyncpg://inkwell:inkwell@db:5432/inkwell PYTHONPATH: /app/src volumes: - ./pyproject.toml:/app/pyproject.toml - ./alembic.ini:/app/alembic.ini - ./alembic:/app/alembic - ./src:/app/src - - thoughtsync-dev-data:/var/thoughtsync + - inkwell-dev-data:/var/inkwell ports: - "5000:5000" # Install deps, wait for the DB, run migrations, then serve with live reload. command: > sh -c "pip install --quiet -e . && - python -m thoughtsync.dbwait && + python -m inkwell.dbwait && alembic upgrade head && - hypercorn 'thoughtsync.app:create_app()' --bind 0.0.0.0:5000 --reload" + hypercorn 'inkwell.app:create_app()' --bind 0.0.0.0:5000 --reload" web: image: node:22-alpine @@ -67,5 +67,5 @@ services: command: sh -c "npm install && npm run dev -- --host 0.0.0.0" volumes: - thoughtsync-dev-db: - thoughtsync-dev-data: + inkwell-dev-db: + inkwell-dev-data: diff --git a/docker-compose.yml b/docker-compose.yml index 1e0fd25..1a2959c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -# ThoughtSync — PRODUCTION stack (app + Postgres). +# Inkwell — PRODUCTION stack (app + Postgres). # # This is the default compose file: `docker compose up -d` runs a real deployment # from the published image. Development lives in docker-compose.dev.yml (hot-reload, @@ -10,15 +10,15 @@ # Per family rule 12 the agent does NOT start this — run it yourself. # # Upgrades: docker compose pull && docker compose up -d -# Rollback: set THOUGHTSYNC_TAG to a commit sha in .env, then the same two commands. +# Rollback: set INKWELL_TAG to a commit sha in .env, then the same two commands. # Every push publishes an immutable : image for exactly this. # # Schema migrations run automatically at container start (see the Dockerfile CMD), # so an upgrade is just a pull and a restart. Take a backup first anyway: # -# docker compose exec -T db pg_dump -U thoughtsync thoughtsync > backup.sql +# docker compose exec -T db pg_dump -U inkwell inkwell > backup.sql # -# Attachments are files, not rows — they live in the `thoughtsync-data` volume and +# Attachments are files, not rows — they live in the `inkwell-data` volume and # a pg_dump does NOT contain them. Back up both or you'll restore notes whose images # are gone. @@ -27,22 +27,21 @@ services: image: postgres:16-alpine restart: unless-stopped environment: - POSTGRES_USER: ${POSTGRES_USER:-thoughtsync} + POSTGRES_USER: ${POSTGRES_USER:-inkwell} # No default on purpose. A production compose that ships a known password is # how self-hosted databases end up in search engines; compose fails fast here # instead, with the message below. POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env — see .env.example} - POSTGRES_DB: ${POSTGRES_DB:-thoughtsync} + POSTGRES_DB: ${POSTGRES_DB:-inkwell} volumes: - # Volume names kept from the previous compose file so an existing deployment - # upgrades in place. Renaming them would silently start against an empty - # database while the old one sat there, orphaned and looking like data loss. - - thoughtsync-db:/var/lib/postgresql/data + # Which volume this resolves to is set at the bottom of this file, and an + # existing deployment overrides it from .env rather than renaming anything. + - inkwell-db:/var/lib/postgresql/data # Deliberately NOT published to the host. The app reaches Postgres over the # compose network; exposing 5432 only widens the attack surface. If you need - # psql, `docker compose exec db psql -U thoughtsync` gets you there without it. + # psql, `docker compose exec db psql -U inkwell` gets you there without it. healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-thoughtsync} -d ${POSTGRES_DB:-thoughtsync}"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-inkwell} -d ${POSTGRES_DB:-inkwell}"] interval: 10s timeout: 5s retries: 10 @@ -56,9 +55,9 @@ services: max-file: "3" app: - # :latest tracks `main`. Set THOUGHTSYNC_TAG=dev in .env to follow the + # :latest tracks `main`. Set INKWELL_TAG=dev in .env to follow the # development line instead, or a commit sha to pin exactly. - image: git.fabledsword.com/bvandeusen/thoughtsync:${THOUGHTSYNC_TAG:-latest} + image: git.fabledsword.com/bvandeusen/inkwell:${INKWELL_TAG:-latest} restart: unless-stopped depends_on: db: @@ -66,11 +65,11 @@ services: environment: # The only required application setting. Everything else a person might want # to tune lives in the admin Settings UI, backed by the database (rule 25). - THOUGHTSYNC_DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-thoughtsync}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-thoughtsync} + INKWELL_DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-inkwell}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-inkwell} volumes: - # Uploaded attachments. /var/thoughtsync is fixed in the app (Config.DATA_DIR), + # Uploaded attachments. /var/inkwell is fixed in the app (Config.DATA_DIR), # not configurable — mount it or lose every image on container recreation. - - thoughtsync-data:/var/thoughtsync + - inkwell-data:/var/inkwell # WHERE THE APP IS REACHABLE FROM. Three shapes, and the right answer is # different for each — the default serves the first. # @@ -86,14 +85,14 @@ services: # and publishing one is a second, unauthenticated way in that bypasses the # proxy — including whatever the proxy is doing about TLS and auth. # - # 3. Reverse proxy on the HOST (not in Docker). Set THOUGHTSYNC_BIND=127.0.0.1 in + # 3. Reverse proxy on the HOST (not in Docker). Set INKWELL_BIND=127.0.0.1 in # .env, so the port exists but only the host itself can reach it. # # If you are exposing this to the internet, you want 2 or 3. Leaving it at 1 # means the app is reachable directly on port 5000, past everything your proxy # does. ports: - - "${THOUGHTSYNC_BIND:-0.0.0.0}:${THOUGHTSYNC_PORT:-5000}:5000" + - "${INKWELL_BIND:-0.0.0.0}:${INKWELL_PORT:-5000}:5000" healthcheck: # python rather than curl: the runtime image is python:3.12-slim and carries no # HTTP client binary. Hits the app's own /api/health. @@ -113,5 +112,12 @@ services: logging: *logging volumes: - thoughtsync-db: - thoughtsync-data: + # Named explicitly so the name is one a person can type, and overridable so a + # deployment that predates the rename to Inkwell keeps the volumes it already has + # (see "Upgrading from ThoughtSync" in .env.example). Pointing these at the wrong + # name does not fail: it starts against an EMPTY database and the old one sits + # there untouched, which looks exactly like data loss. + inkwell-db: + name: ${INKWELL_DB_VOLUME:-inkwell-db} + inkwell-data: + name: ${INKWELL_DATA_VOLUME:-inkwell-data} diff --git a/docs/sync.md b/docs/sync.md index d9e6e46..2a2476e 100644 --- a/docs/sync.md +++ b/docs/sync.md @@ -20,7 +20,7 @@ an older release than the desktop app for months. So the wire protocol is versioned **separately from either program's release version**, and each side declares two numbers: what it speaks, and the oldest counterpart it accepts. -| | server (`src/thoughtsync/sync.py`) | client (`desktop/src-tauri/src/sync/compat.rs`) | +| | server (`src/inkwell/sync.py`) | client (`desktop/src-tauri/src/sync/compat.rs`) | |---|---|---| | speaks | `SYNC_PROTOCOL_VERSION` | `CLIENT_PROTOCOL_VERSION` | | accepts down to | `MIN_CLIENT_PROTOCOL_VERSION` | `MIN_SERVER_PROTOCOL_VERSION` | diff --git a/frontend/src/notes/markdown.ts b/frontend/src/notes/markdown.ts index 6aec336..d9e841a 100644 --- a/frontend/src/notes/markdown.ts +++ b/frontend/src/notes/markdown.ts @@ -75,7 +75,7 @@ export function parseInline(text: string): InlineToken[] { } // A checklist item: the third implementation of one grammar, alongside -// core/src/local/derive.rs and src/thoughtsync/notes/checklist.py. A difference +// core/src/local/derive.rs and src/inkwell/notes/checklist.py. A difference // between any two of them is a checklist that changes shape when it syncs (M304). // // `-` and `*` only, deliberately, even though the `ul` matcher below also takes `+`. diff --git a/packaging/fetch-clients.sh b/packaging/fetch-clients.sh index 030a6c4..6d74096 100755 --- a/packaging/fetch-clients.sh +++ b/packaging/fetch-clients.sh @@ -37,7 +37,7 @@ case "$channel" in dev|stable) : ;; *) esac SERVER="${GITHUB_SERVER_URL:-https://git.fabledsword.com}" -REPO="${GITHUB_REPOSITORY:-bvandeusen/thoughtsync}" +REPO="${GITHUB_REPOSITORY:-bvandeusen/inkwell}" # The channel's release tag, not its name — `dev` lives on `dev-rolling` # (packaging/channel-tag.sh, Scribe #2184). tag="$(sh "$(dirname "$0")/channel-tag.sh" "$channel")" @@ -88,15 +88,15 @@ echo "==> Collecting the $channel clients" # Its sidecar is published whole by the Android lane — an APK keeps its version in # a binary AXML manifest, so the values are recorded where they were already known. # Copied verbatim rather than rebuilt here. -if fetch "$BASE/thoughtsync.apk" "$dest/thoughtsync.apk" && - fetch "$BASE/thoughtsync-android.json" "$dest/thoughtsync-android.json"; then - echo " android $(field "$dest/thoughtsync-android.json" version_name)" +if fetch "$BASE/thoughtsync.apk" "$dest/inkwell.apk" && + fetch "$BASE/thoughtsync-android.json" "$dest/inkwell-android.json"; then + echo " android $(field "$dest/inkwell-android.json" version_name)" else # Both or neither. Half a pair is worse than none: the server would read a # sidecar describing an APK that is not there, or an APK it cannot state a # version for. echo "::warning::No Android client on the $channel channel — this image ships without one." - rm -f "$dest/thoughtsync.apk" "$dest/thoughtsync-android.json" + rm -f "$dest/inkwell.apk" "$dest/inkwell-android.json" fi # --- desktop ----------------------------------------------------------------- @@ -132,10 +132,10 @@ bake_desktop() { # # `||` for row in \ - "linux-deb|ThoughtSync_${key}_amd64.deb|thoughtsync.deb" \ - "linux-pacman|thoughtsync-${key}-1-x86_64.pkg.tar.zst|thoughtsync.pkg.tar.zst" \ - "linux-appimage|ThoughtSync_${key}_amd64.AppImage|thoughtsync.AppImage" \ - "windows|ThoughtSync_${key}_x64-setup.exe|thoughtsync-setup.exe" + "linux-deb|ThoughtSync_${key}_amd64.deb|inkwell.deb" \ + "linux-pacman|thoughtsync-${key}-1-x86_64.pkg.tar.zst|inkwell.pkg.tar.zst" \ + "linux-appimage|ThoughtSync_${key}_amd64.AppImage|inkwell.AppImage" \ + "windows|ThoughtSync_${key}_x64-setup.exe|inkwell-setup.exe" do id="${row%%|*}"; rest="${row#*|}" remote="${rest%%|*}"; local_name="${rest#*|}" @@ -158,7 +158,7 @@ bake_desktop() { fi fi - sidecar "$dest/$local_name" "$dest/thoughtsync-$id.json" "$name" "$key" + sidecar "$dest/$local_name" "$dest/inkwell-$id.json" "$name" "$key" echo " $id $(bytes "$dest/$local_name") bytes" done } diff --git a/packaging/guard-forward.sh b/packaging/guard-forward.sh index e20f3e2..861a0ff 100755 --- a/packaging/guard-forward.sh +++ b/packaging/guard-forward.sh @@ -32,7 +32,7 @@ set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd)" SERVER="${GITHUB_SERVER_URL:-https://git.fabledsword.com}" -REPO="${GITHUB_REPOSITORY:-bvandeusen/thoughtsync}" +REPO="${GITHUB_REPOSITORY:-bvandeusen/inkwell}" artifact="${1:?usage: guard-forward.sh }" diff --git a/pyproject.toml b/pyproject.toml index 38cf1e8..230c15f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools>=68.0"] build-backend = "setuptools.build_meta" [project] -name = "thoughtsync" +name = "inkwell" version = "0.2.0" description = "Self-hosted personal thought-capture web app (FabledSword family)" requires-python = ">=3.12" diff --git a/src/thoughtsync/__init__.py b/src/inkwell/__init__.py similarity index 90% rename from src/thoughtsync/__init__.py rename to src/inkwell/__init__.py index 0148171..599783b 100644 --- a/src/thoughtsync/__init__.py +++ b/src/inkwell/__init__.py @@ -1,4 +1,4 @@ -"""ThoughtSync — self-hosted personal thought-capture web app (FabledSword family).""" +"""Inkwell — self-hosted personal thought-capture web app (FabledSword family).""" # PACKAGING METADATA, and nothing else. Not the version any running server reports. # diff --git a/src/thoughtsync/acl.py b/src/inkwell/acl.py similarity index 100% rename from src/thoughtsync/acl.py rename to src/inkwell/acl.py diff --git a/src/thoughtsync/app.py b/src/inkwell/app.py similarity index 99% rename from src/thoughtsync/app.py rename to src/inkwell/app.py index bd2ed75..44fa4e5 100644 --- a/src/thoughtsync/app.py +++ b/src/inkwell/app.py @@ -32,7 +32,7 @@ from .sync import bp as sync_bp, protocol_advertisement # `force=False` (the default) so a host that has already configured logging keeps its # own setup; LOG_LEVEL lets an operator turn it up without a code change. logging.basicConfig( - level=os.environ.get("THOUGHTSYNC_LOG_LEVEL", "INFO").upper(), + level=os.environ.get("INKWELL_LOG_LEVEL", "INFO").upper(), format="%(asctime)s %(levelname)s %(name)s %(message)s", ) diff --git a/src/thoughtsync/auth.py b/src/inkwell/auth.py similarity index 100% rename from src/thoughtsync/auth.py rename to src/inkwell/auth.py diff --git a/src/thoughtsync/client_dist.py b/src/inkwell/client_dist.py similarity index 96% rename from src/thoughtsync/client_dist.py rename to src/inkwell/client_dist.py index 6cba4be..ff10ed9 100644 --- a/src/thoughtsync/client_dist.py +++ b/src/inkwell/client_dist.py @@ -38,8 +38,8 @@ wins — would mean replacing one client silently retracts the other four. Two files, and both must be present: -- the artifact, at a FIXED name (`thoughtsync.deb`, not - `ThoughtSync_2026.08.30.0307_amd64.deb`) +- the artifact, at a FIXED name (`inkwell.deb`, not + `Inkwell_2026.08.30.0307_amd64.deb`) - its sidecar, `{version_name, version_code, size, sha256}` **Fixed names, and the version only in the sidecar.** A version-stamped filename @@ -53,7 +53,7 @@ an APK keeps it in a binary AXML manifest Python cannot parse without the Androi build tools, and a `.deb` or an AppImage would each need a different unpacker. CI writes the sidecar where the real value is already known. -The AppImage needs a THIRD file, `thoughtsync.AppImage.sig`. That is the minisign +The AppImage needs a THIRD file, `inkwell.AppImage.sig`. That is the minisign signature the desktop updater verifies before replacing the running binary, and a bundle that cannot be verified cannot be offered as an update — so a missing signature makes the AppImage absent rather than merely unsigned. @@ -132,31 +132,31 @@ PLATFORMS: tuple[Platform, ...] = ( # exact names, CI bakes them in under them, and clients in the field poll # `/api/client/android`. Renaming them to match the pattern below would # buy tidiness and strand every installed phone. - artifact="thoughtsync.apk", - sidecar="thoughtsync-android.json", + artifact="inkwell.apk", + sidecar="inkwell-android.json", mimetype="application/vnd.android.package-archive", ), Platform( id="linux-deb", label="Debian / Ubuntu", - artifact="thoughtsync.deb", - sidecar="thoughtsync-linux-deb.json", + artifact="inkwell.deb", + sidecar="inkwell-linux-deb.json", mimetype="application/vnd.debian.binary-package", code_is_int=False, ), Platform( id="linux-pacman", label="Arch / CachyOS", - artifact="thoughtsync.pkg.tar.zst", - sidecar="thoughtsync-linux-pacman.json", + artifact="inkwell.pkg.tar.zst", + sidecar="inkwell-linux-pacman.json", mimetype="application/zstd", code_is_int=False, ), Platform( id="linux-appimage", label="Other Linux (AppImage)", - artifact="thoughtsync.AppImage", - sidecar="thoughtsync-linux-appimage.json", + artifact="inkwell.AppImage", + sidecar="inkwell-linux-appimage.json", # No registered type for an AppImage, and guessing one buys nothing: it is # served as an attachment either way, and octet-stream is the answer that # cannot be wrong. @@ -167,8 +167,8 @@ PLATFORMS: tuple[Platform, ...] = ( Platform( id="windows", label="Windows", - artifact="thoughtsync-setup.exe", - sidecar="thoughtsync-windows.json", + artifact="inkwell-setup.exe", + sidecar="inkwell-windows.json", mimetype="application/vnd.microsoft.portable-executable", code_is_int=False, ), diff --git a/src/thoughtsync/colors.py b/src/inkwell/colors.py similarity index 100% rename from src/thoughtsync/colors.py rename to src/inkwell/colors.py diff --git a/src/thoughtsync/common.py b/src/inkwell/common.py similarity index 100% rename from src/thoughtsync/common.py rename to src/inkwell/common.py diff --git a/src/thoughtsync/config.py b/src/inkwell/config.py similarity index 72% rename from src/thoughtsync/config.py rename to src/inkwell/config.py index 690e177..a801c7b 100644 --- a/src/thoughtsync/config.py +++ b/src/inkwell/config.py @@ -7,26 +7,26 @@ from pathlib import Path class Config: """Bootstrap configuration. - For a basic install, ``THOUGHTSYNC_DATABASE_URL`` is the ONLY required env var — + For a basic install, ``INKWELL_DATABASE_URL`` is the ONLY required env var — every other tunable lives in the DB-backed Settings UI (rule 25). The only other env var is an optional "break-glass" item: - - ``THOUGHTSYNC_SECRET_KEY`` — optional override for the cookie-signing secret. + - ``INKWELL_SECRET_KEY`` — optional override for the cookie-signing secret. If unset, a key is generated and persisted in the DB (see - ``thoughtsync.settings.load_or_create_secret_key``), so sessions survive + ``inkwell.settings.load_or_create_secret_key``), so sessions survive restarts with no volume required. Uploaded media lives under ``DATA_DIR`` — a fixed, authoritative path - (``/var/thoughtsync``), intentionally NOT configurable (a mutable data path only + (``/var/inkwell``), intentionally NOT configurable (a mutable data path only invites breakage). Mount a volume there if you want uploads to persist across container recreation; a text-notes-only install never writes to it. """ - DATA_DIR = "/var/thoughtsync" + DATA_DIR = "/var/inkwell" DATABASE_URL = os.environ.get( - "THOUGHTSYNC_DATABASE_URL", - "postgresql+asyncpg://thoughtsync:thoughtsync@localhost:5432/thoughtsync", + "INKWELL_DATABASE_URL", + "postgresql+asyncpg://inkwell:inkwell@localhost:5432/inkwell", ) @classmethod @@ -50,5 +50,5 @@ class Config: @classmethod def secret_key_env(cls) -> str | None: """Optional break-glass override for the cookie-signing secret.""" - return os.environ.get("THOUGHTSYNC_SECRET_KEY") or None + return os.environ.get("INKWELL_SECRET_KEY") or None diff --git a/src/thoughtsync/db.py b/src/inkwell/db.py similarity index 100% rename from src/thoughtsync/db.py rename to src/inkwell/db.py diff --git a/src/thoughtsync/dbwait.py b/src/inkwell/dbwait.py similarity index 100% rename from src/thoughtsync/dbwait.py rename to src/inkwell/dbwait.py diff --git a/src/thoughtsync/labeling.py b/src/inkwell/labeling.py similarity index 100% rename from src/thoughtsync/labeling.py rename to src/inkwell/labeling.py diff --git a/src/thoughtsync/labels.py b/src/inkwell/labels.py similarity index 100% rename from src/thoughtsync/labels.py rename to src/inkwell/labels.py diff --git a/src/thoughtsync/models/__init__.py b/src/inkwell/models/__init__.py similarity index 100% rename from src/thoughtsync/models/__init__.py rename to src/inkwell/models/__init__.py diff --git a/src/thoughtsync/models/all.py b/src/inkwell/models/all.py similarity index 100% rename from src/thoughtsync/models/all.py rename to src/inkwell/models/all.py diff --git a/src/thoughtsync/models/device_token.py b/src/inkwell/models/device_token.py similarity index 100% rename from src/thoughtsync/models/device_token.py rename to src/inkwell/models/device_token.py diff --git a/src/thoughtsync/models/group.py b/src/inkwell/models/group.py similarity index 96% rename from src/thoughtsync/models/group.py rename to src/inkwell/models/group.py index 857089f..eb88a6f 100644 --- a/src/thoughtsync/models/group.py +++ b/src/inkwell/models/group.py @@ -12,7 +12,7 @@ from . import Base class Group(Base): """A named set of users. Sharing a resource with a group grants it to every - member (see thoughtsync.acl.visible_to_user).""" + member (see inkwell.acl.visible_to_user).""" __tablename__ = "groups" diff --git a/src/thoughtsync/models/label.py b/src/inkwell/models/label.py similarity index 100% rename from src/thoughtsync/models/label.py rename to src/inkwell/models/label.py diff --git a/src/thoughtsync/models/note.py b/src/inkwell/models/note.py similarity index 100% rename from src/thoughtsync/models/note.py rename to src/inkwell/models/note.py diff --git a/src/thoughtsync/models/note_attachment.py b/src/inkwell/models/note_attachment.py similarity index 100% rename from src/thoughtsync/models/note_attachment.py rename to src/inkwell/models/note_attachment.py diff --git a/src/thoughtsync/models/note_link_preview.py b/src/inkwell/models/note_link_preview.py similarity index 100% rename from src/thoughtsync/models/note_link_preview.py rename to src/inkwell/models/note_link_preview.py diff --git a/src/thoughtsync/models/note_revision.py b/src/inkwell/models/note_revision.py similarity index 100% rename from src/thoughtsync/models/note_revision.py rename to src/inkwell/models/note_revision.py diff --git a/src/thoughtsync/models/saved_filter.py b/src/inkwell/models/saved_filter.py similarity index 100% rename from src/thoughtsync/models/saved_filter.py rename to src/inkwell/models/saved_filter.py diff --git a/src/thoughtsync/models/settings.py b/src/inkwell/models/settings.py similarity index 88% rename from src/thoughtsync/models/settings.py rename to src/inkwell/models/settings.py index 7525a90..e1fe3a5 100644 --- a/src/thoughtsync/models/settings.py +++ b/src/inkwell/models/settings.py @@ -10,7 +10,7 @@ from . import Base class Setting(Base): """A single persisted key/value setting. Values are JSON-encoded text; the - typed defaults + metadata live in the code registry (thoughtsync.settings), so + typed defaults + metadata live in the code registry (inkwell.settings), so an empty table means "all defaults" (rule 26).""" __tablename__ = "settings" diff --git a/src/thoughtsync/models/share.py b/src/inkwell/models/share.py similarity index 100% rename from src/thoughtsync/models/share.py rename to src/inkwell/models/share.py diff --git a/src/thoughtsync/models/user.py b/src/inkwell/models/user.py similarity index 100% rename from src/thoughtsync/models/user.py rename to src/inkwell/models/user.py diff --git a/src/thoughtsync/notes/__init__.py b/src/inkwell/notes/__init__.py similarity index 99% rename from src/thoughtsync/notes/__init__.py rename to src/inkwell/notes/__init__.py index 9215963..5488dfa 100644 --- a/src/thoughtsync/notes/__init__.py +++ b/src/inkwell/notes/__init__.py @@ -6,7 +6,7 @@ small text/query helpers (`helpers`), and export/import (`import_export`). The route handlers themselves stay here so blueprint registration is in one place, and `bp` is defined in `_bp` so every module can import it without a cycle. -External callers import from `thoughtsync.notes` (see `__all__`); those names are +External callers import from `inkwell.notes` (see `__all__`); those names are re-exported here so the package is a drop-in replacement for the old module.""" from __future__ import annotations @@ -237,7 +237,7 @@ async def export_notes(): ).all() payload: dict = { - "app": "thoughtsync", + "app": "inkwell", "version": 1, "exported_at": datetime.now(timezone.utc).isoformat(), "labels": [{"name": lb.name, "color": lb.color} for lb in all_labels], @@ -278,7 +278,7 @@ async def export_notes(): data, headers={ "Content-Type": "application/zip", - "Content-Disposition": f'attachment; filename="thoughtsync-export-{stamp}.zip"', + "Content-Disposition": f'attachment; filename="inkwell-export-{stamp}.zip"', }, ) diff --git a/src/thoughtsync/notes/_bp.py b/src/inkwell/notes/_bp.py similarity index 100% rename from src/thoughtsync/notes/_bp.py rename to src/inkwell/notes/_bp.py diff --git a/src/thoughtsync/notes/checklist.py b/src/inkwell/notes/checklist.py similarity index 98% rename from src/thoughtsync/notes/checklist.py rename to src/inkwell/notes/checklist.py index 1c92286..04a29df 100644 --- a/src/thoughtsync/notes/checklist.py +++ b/src/inkwell/notes/checklist.py @@ -15,7 +15,7 @@ THE GRAMMAR IS SHARED. Three implementations exist and they have to agree, becau difference between any two of them is a checklist that changes shape when it syncs: core/src/local/derive.rs the native clients (desktop + Android) - src/thoughtsync/notes/checklist.py this file, the server + src/inkwell/notes/checklist.py this file, the server frontend/src/notes/markdown.ts the browser optional indent, `-` or `*`, one-or-more spaces, `[ ]`/`[x]`/`[X]`, diff --git a/src/thoughtsync/notes/helpers.py b/src/inkwell/notes/helpers.py similarity index 100% rename from src/thoughtsync/notes/helpers.py rename to src/inkwell/notes/helpers.py diff --git a/src/thoughtsync/notes/import_export.py b/src/inkwell/notes/import_export.py similarity index 92% rename from src/thoughtsync/notes/import_export.py rename to src/inkwell/notes/import_export.py index 5cdd9da..5b0c3e1 100644 --- a/src/thoughtsync/notes/import_export.py +++ b/src/inkwell/notes/import_export.py @@ -1,5 +1,5 @@ """Export/import helpers (the non-route logic). Export renders each note as Markdown; -import normalizes a ThoughtSync export OR a Google Keep Takeout zip into a common spec +import normalizes an Inkwell export OR a Google Keep Takeout zip into a common spec and materializes notes — with a decompression-size budget so an import zip bomb can't exhaust memory/disk.""" from __future__ import annotations @@ -58,7 +58,13 @@ def _note_markdown(note: Note, labels: list) -> str: return "\n".join(fm) + "\n" -# --- Import: ThoughtSync's own export (round-trip) OR a Google Keep Takeout zip --- +# --- Import: Inkwell's own export (round-trip) OR a Google Keep Takeout zip --- + +# The `app` value an export's notes.json carries. "thoughtsync" is what every export +# written before the rename to Inkwell says (Scribe note 5071), and those zips are +# people's backups, so they keep importing. That is the one reason the old name is +# still read here; nothing writes it any more. +NATIVE_APP_MARKERS = ("inkwell", "thoughtsync") # Reverse of ALLOWED_IMAGE_MIMES, for inferring an attachment's mime from its # filename when the source didn't record one (Keep usually does; be defensive). @@ -75,7 +81,7 @@ def _usec_to_dt(usec: object) -> datetime | None: def _native_spec(n: dict) -> dict: - """Normalize one note from a ThoughtSync export's notes.json into the common + """Normalize one note from an Inkwell export's notes.json into the common import spec consumed by _create_imported_note.""" return { "title": n.get("title"), @@ -186,9 +192,10 @@ class _ImportBudget: def _read_import_specs(zf: zipfile.ZipFile, budget: _ImportBudget) -> tuple[list[dict], str]: - """Detect the archive format and return (specs, source). A ThoughtSync export - is recognized by its notes.json (app == thoughtsync); otherwise each Keep-shaped - .json is imported. Returns ([], "") when nothing importable is found.""" + """Detect the archive format and return (specs, source). An Inkwell export + is recognized by its notes.json (app in NATIVE_APP_MARKERS); otherwise each + Keep-shaped .json is imported. Returns ([], "") when nothing importable + is found.""" names = zf.namelist() for name in names: if posixpath.basename(name) == "notes.json": @@ -196,9 +203,9 @@ def _read_import_specs(zf: zipfile.ZipFile, budget: _ImportBudget) -> tuple[list doc = json.loads(budget.read(zf, name)) except (ValueError, KeyError): continue - if isinstance(doc, dict) and doc.get("app") == "thoughtsync": + if isinstance(doc, dict) and doc.get("app") in NATIVE_APP_MARKERS: specs = [_native_spec(n) for n in (doc.get("notes") or []) if isinstance(n, dict)] - return specs, "thoughtsync" + return specs, "inkwell" keep_specs: list[dict] = [] keep_keys = ("textContent", "listContent", "isPinned", "isArchived", "isTrashed", "userEditedTimestampUsec") @@ -255,7 +262,7 @@ async def _create_imported_note( body = spec.get("body") or "" # An imported title becomes the note's FIRST BODY LINE. # - # ThoughtSync has no title field any more (M13 step 3), but the things people + # Inkwell has no title field any more (M13 step 3), but the things people # import from do — Keep notes carry one, and so does any export taken before this. # Dropping it would silently lose text someone wrote; folding it into the body puts # it exactly where a name now lives, so the note comes in named the way it was. diff --git a/src/thoughtsync/notes/recurrence.py b/src/inkwell/notes/recurrence.py similarity index 100% rename from src/thoughtsync/notes/recurrence.py rename to src/inkwell/notes/recurrence.py diff --git a/src/thoughtsync/notes/serialize.py b/src/inkwell/notes/serialize.py similarity index 100% rename from src/thoughtsync/notes/serialize.py rename to src/inkwell/notes/serialize.py diff --git a/src/thoughtsync/notes/tags.py b/src/inkwell/notes/tags.py similarity index 100% rename from src/thoughtsync/notes/tags.py rename to src/inkwell/notes/tags.py diff --git a/src/thoughtsync/proxy.py b/src/inkwell/proxy.py similarity index 100% rename from src/thoughtsync/proxy.py rename to src/inkwell/proxy.py diff --git a/src/thoughtsync/ratelimit.py b/src/inkwell/ratelimit.py similarity index 100% rename from src/thoughtsync/ratelimit.py rename to src/inkwell/ratelimit.py diff --git a/src/thoughtsync/responses.py b/src/inkwell/responses.py similarity index 100% rename from src/thoughtsync/responses.py rename to src/inkwell/responses.py diff --git a/src/thoughtsync/retention.py b/src/inkwell/retention.py similarity index 100% rename from src/thoughtsync/retention.py rename to src/inkwell/retention.py diff --git a/src/thoughtsync/revisions.py b/src/inkwell/revisions.py similarity index 100% rename from src/thoughtsync/revisions.py rename to src/inkwell/revisions.py diff --git a/src/thoughtsync/saved_filters.py b/src/inkwell/saved_filters.py similarity index 100% rename from src/thoughtsync/saved_filters.py rename to src/inkwell/saved_filters.py diff --git a/src/thoughtsync/security.py b/src/inkwell/security.py similarity index 100% rename from src/thoughtsync/security.py rename to src/inkwell/security.py diff --git a/src/thoughtsync/serialize.py b/src/inkwell/serialize.py similarity index 100% rename from src/thoughtsync/serialize.py rename to src/inkwell/serialize.py diff --git a/src/thoughtsync/settings.py b/src/inkwell/settings.py similarity index 100% rename from src/thoughtsync/settings.py rename to src/inkwell/settings.py diff --git a/src/thoughtsync/settings_api.py b/src/inkwell/settings_api.py similarity index 100% rename from src/thoughtsync/settings_api.py rename to src/inkwell/settings_api.py diff --git a/src/thoughtsync/sync.py b/src/inkwell/sync.py similarity index 100% rename from src/thoughtsync/sync.py rename to src/inkwell/sync.py diff --git a/src/thoughtsync/unfurl.py b/src/inkwell/unfurl.py similarity index 100% rename from src/thoughtsync/unfurl.py rename to src/inkwell/unfurl.py diff --git a/src/thoughtsync/unfurl_queue.py b/src/inkwell/unfurl_queue.py similarity index 100% rename from src/thoughtsync/unfurl_queue.py rename to src/inkwell/unfurl_queue.py diff --git a/tests/conftest.py b/tests/conftest.py index 817c4ff..79144c4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,12 @@ import pytest -from thoughtsync.config import Config +from inkwell.config import Config @pytest.fixture(autouse=True) def _isolated_data_dir(tmp_path, monkeypatch): """Keep any media writes on an isolated temp dir rather than the fixed - /var/thoughtsync. DB-free unit tests never actually hit it (create_app takes its + /var/inkwell. DB-free unit tests never actually hit it (create_app takes its signing key from env-or-random, not a file), but this stays defensive.""" monkeypatch.setattr(Config, "DATA_DIR", str(tmp_path / "data")) yield diff --git a/tests/test_acl.py b/tests/test_acl.py index cf7c7b9..715c473 100644 --- a/tests/test_acl.py +++ b/tests/test_acl.py @@ -1,7 +1,7 @@ import uuid -from thoughtsync.acl import visible_to_user -from thoughtsync.models.user import User +from inkwell.acl import visible_to_user +from inkwell.models.user import User def test_visible_to_user_builds_owner_or_shared_predicate(): diff --git a/tests/test_admin.py b/tests/test_admin.py index 09109dd..3c53f77 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -1,6 +1,6 @@ import pytest -from thoughtsync.app import create_app +from inkwell.app import create_app @pytest.fixture diff --git a/tests/test_app.py b/tests/test_app.py index e9f95ec..636a7af 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,6 +1,6 @@ import pytest -from thoughtsync.app import create_app +from inkwell.app import create_app @pytest.fixture @@ -61,7 +61,7 @@ async def test_no_version_in_the_environment_reports_unknown(monkeypatch): happens to hold, so bumping the packaging version cannot make this pass for the wrong reason. """ - from thoughtsync import __version__ + from inkwell import __version__ monkeypatch.delenv("APP_VERSION", raising=False) reported = await reported_version() diff --git a/tests/test_client_dist.py b/tests/test_client_dist.py index 812e04a..00a0503 100644 --- a/tests/test_client_dist.py +++ b/tests/test_client_dist.py @@ -3,9 +3,9 @@ from pathlib import Path import pytest -from thoughtsync import client_dist -from thoughtsync.app import create_app -from thoughtsync.client_dist import ( +from inkwell import client_dist +from inkwell.app import create_app +from inkwell.client_dist import ( APK_NAME, BY_ID, MANIFEST_NAME, @@ -15,7 +15,7 @@ from thoughtsync.client_dist import ( release, releases, ) -from thoughtsync.config import Config +from inkwell.config import Config # DB-free, like the rest of this suite — the test lane runs no Postgres. That is # why the advertisement is asserted through `advertisement()` rather than through @@ -61,7 +61,7 @@ def coded(platform_id: str, value: int): def _empty_baked_client(tmp_path, monkeypatch): """Point the baked-in copy at an empty directory. - In a source checkout `src/thoughtsync/client/` does not exist, so these tests + In a source checkout `src/inkwell/client/` does not exist, so these tests would pass anyway — but only by accident of where they are run. A built image has real clients there, and a test that silently depends on which tree it is in is one that will eventually lie. @@ -118,13 +118,17 @@ def test_every_platform_has_its_own_filenames(): assert not set(artifacts) & set(sidecars) -def test_the_android_names_are_the_ones_already_published(): - """Pinned because renaming them is a tidy-up that strands every installed phone. +def test_the_android_names_are_the_ones_the_image_build_writes(): + """Pinned against `packaging/fetch-clients.sh`, which writes the APK and its + sidecar under these names when it bakes them into the image. The two are a pair + with nothing checking them against each other, so a rename on one side reads as + "no Android client" on the other rather than as an error. - The Android lane publishes these exact names and CI bakes them in under them. + Phones never ask for these names; they poll `/api/client/android`. The names + changed once, with the rename to Inkwell (Scribe note 5071). """ - assert APK_NAME == "thoughtsync.apk" - assert MANIFEST_NAME == "thoughtsync-android.json" + assert APK_NAME == "inkwell.apk" + assert MANIFEST_NAME == "inkwell-android.json" # --- absence is an ordinary answer ------------------------------------------- diff --git a/tests/test_devices.py b/tests/test_devices.py index 24aa529..1d2052c 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -1,6 +1,6 @@ import pytest -from thoughtsync.app import create_app +from inkwell.app import create_app @pytest.fixture diff --git a/tests/test_integration.py b/tests/test_integration.py index d926738..0be5bb2 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -22,20 +22,20 @@ import pytest import pytest_asyncio from sqlalchemy import select, text -from thoughtsync import ratelimit -from thoughtsync.app import create_app -from thoughtsync.db import dispose_engine, session_scope -from thoughtsync.models.label import NoteLabel -from thoughtsync.models.note import Note -from thoughtsync.models.user import User -from thoughtsync.notes.tags import _lift_and_reconcile_tags -from thoughtsync.settings import get_setting, live, refresh_live, reset_live, set_settings -from thoughtsync.notes.checklist import parse_items, set_item_checked -from thoughtsync.notes.helpers import derive_display_title -from thoughtsync.models.note_link_preview import NoteLinkPreview -from thoughtsync.models.note_revision import NoteRevision -from thoughtsync.revisions import REVISION_WINDOW_MINUTES, should_snapshot -from thoughtsync.unfurl_queue import _unfurl_new_urls, detect_urls +from inkwell import ratelimit +from inkwell.app import create_app +from inkwell.db import dispose_engine, session_scope +from inkwell.models.label import NoteLabel +from inkwell.models.note import Note +from inkwell.models.user import User +from inkwell.notes.tags import _lift_and_reconcile_tags +from inkwell.settings import get_setting, live, refresh_live, reset_live, set_settings +from inkwell.notes.checklist import parse_items, set_item_checked +from inkwell.notes.helpers import derive_display_title +from inkwell.models.note_link_preview import NoteLinkPreview +from inkwell.models.note_revision import NoteRevision +from inkwell.revisions import REVISION_WINDOW_MINUTES, should_snapshot +from inkwell.unfurl_queue import _unfurl_new_urls, detect_urls pytestmark = pytest.mark.integration @@ -305,7 +305,7 @@ async def test_auto_unfurl_stores_a_preview_and_skips_what_is_cached(db, owner, calls.append(url) return {"url": url, "title": f"T {url}", "description": None, "image_url": None, "site_name": "example.com"} - monkeypatch.setattr("thoughtsync.unfurl_queue.unfurl", fake_unfurl) + monkeypatch.setattr("inkwell.unfurl_queue.unfurl", fake_unfurl) await _unfurl_new_urls(note.id, note.body) assert sorted(calls) == ["https://example.com/a", "https://example.com/b"] @@ -331,7 +331,7 @@ async def test_auto_unfurl_drops_a_preview_whose_url_left_the_body(db, owner, mo # Simulate the body changing while the request was in the air. return {"url": url, "title": "T", "description": None, "image_url": None, "site_name": None} - monkeypatch.setattr("thoughtsync.unfurl_queue.unfurl", fake_unfurl) + monkeypatch.setattr("inkwell.unfurl_queue.unfurl", fake_unfurl) note.body = "changed my mind" await db.commit() @@ -352,7 +352,7 @@ async def test_detection_agrees_with_what_gets_stored(db, owner, monkeypatch): async def fake_unfurl(url): return {"url": url, "title": "T", "description": None, "image_url": None, "site_name": None} - monkeypatch.setattr("thoughtsync.unfurl_queue.unfurl", fake_unfurl) + monkeypatch.setattr("inkwell.unfurl_queue.unfurl", fake_unfurl) await _unfurl_new_urls(note.id, body) stored = { diff --git a/tests/test_labels.py b/tests/test_labels.py index 85d28f2..10e0085 100644 --- a/tests/test_labels.py +++ b/tests/test_labels.py @@ -2,9 +2,9 @@ import uuid import pytest -from thoughtsync.app import create_app -from thoughtsync.models.label import Label -from thoughtsync.serialize import serialize_label +from inkwell.app import create_app +from inkwell.models.label import Label +from inkwell.serialize import serialize_label @pytest.fixture diff --git a/tests/test_notes.py b/tests/test_notes.py index e7878bd..75addcb 100644 --- a/tests/test_notes.py +++ b/tests/test_notes.py @@ -1,12 +1,15 @@ +import io +import json +import zipfile from datetime import datetime, timezone import pytest -from thoughtsync.app import create_app -from thoughtsync.common import coerce_bool, parse_dt -from thoughtsync.colors import NOTE_COLORS -from thoughtsync.models.note import Note -from thoughtsync.notes.checklist import ( +from inkwell.app import create_app +from inkwell.common import coerce_bool, parse_dt +from inkwell.colors import NOTE_COLORS +from inkwell.models.note import Note +from inkwell.notes.checklist import ( append_item, parse_items, remove_item, @@ -14,13 +17,15 @@ from thoughtsync.notes.checklist import ( set_item_text, strip_marker, ) -from thoughtsync.unfurl_queue import detect_urls -from thoughtsync.notes import ( +from inkwell.unfurl_queue import detect_urls +from inkwell.notes import ( + _ImportBudget, _attachment_ext, _header_filename, _keep_spec, _native_spec, _note_markdown, + _read_import_specs, _safe_filename, _slugify, _usec_to_dt, @@ -32,7 +37,8 @@ from thoughtsync.notes import ( parse_list_items, parse_tags, ) -from thoughtsync.notes.tags import split_body_tags +from inkwell.notes.import_export import NATIVE_APP_MARKERS +from inkwell.notes.tags import split_body_tags @pytest.fixture @@ -672,3 +678,24 @@ def test_native_spec_of_a_pre_m304_export_still_carries_its_items(): # items at all. old = {"body": "shopping", "items": [{"text": "milk", "checked": True}]} assert _native_spec(old)["items"] == [{"text": "milk", "checked": True}] + + +def _export_zip(app_marker: str) -> zipfile.ZipFile: + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w") as zf: + zf.writestr("notes.json", json.dumps({"app": app_marker, "notes": [{"body": "kept"}]})) + return zipfile.ZipFile(io.BytesIO(buf.getvalue())) + + +@pytest.mark.parametrize("marker", ["inkwell", "thoughtsync"]) +def test_an_export_imports_under_either_name(marker): + # "thoughtsync" is what every export written before the rename to Inkwell says. + # Those zips are backups, so they have to keep restoring as native exports rather + # than falling through to the Keep importer, which would find nothing in them. + specs, source = _read_import_specs(_export_zip(marker), _ImportBudget()) + assert source == "inkwell" + assert [s["body"] for s in specs] == ["kept"] + + +def test_new_exports_are_written_as_inkwell(): + assert NATIVE_APP_MARKERS[0] == "inkwell" diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 75e3a1f..62c9997 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -4,8 +4,8 @@ The whole security property is "a caller cannot forge their own address", and it on counting in from the RIGHT of the header rather than the left. These are the cases that tell the two apart — pure functions, no request context, no database. """ -from thoughtsync.proxy import forwarded_for, trusted_entry -from thoughtsync.settings import live +from inkwell.proxy import forwarded_for, trusted_entry +from inkwell.settings import live PEER = "10.0.0.1" # the socket address: our own proxy, or the caller when unproxied diff --git a/tests/test_ratelimit.py b/tests/test_ratelimit.py index 4abd4c5..167e325 100644 --- a/tests/test_ratelimit.py +++ b/tests/test_ratelimit.py @@ -12,10 +12,10 @@ import time import pytest -from thoughtsync import ratelimit -from thoughtsync.settings import live -from thoughtsync.app import create_app -from thoughtsync.ratelimit import SlidingWindow +from inkwell import ratelimit +from inkwell.settings import live +from inkwell.app import create_app +from inkwell.ratelimit import SlidingWindow def window(limit: int, window_s: float) -> SlidingWindow: diff --git a/tests/test_retention.py b/tests/test_retention.py index 2e19ca6..3b68a36 100644 --- a/tests/test_retention.py +++ b/tests/test_retention.py @@ -2,14 +2,14 @@ from datetime import datetime, timedelta, timezone import pytest -from thoughtsync.retention import ( +from inkwell.retention import ( SWEEP_BATCH, SWEEP_INTERVAL_SECONDS, SWEEP_STARTUP_DELAY_SECONDS, expired_before, sweep_expired_trash, ) -from thoughtsync.settings import REGISTRY, get_public_config, validate_updates +from inkwell.settings import REGISTRY, get_public_config, validate_updates NOW = datetime(2026, 7, 26, 12, 0, tzinfo=timezone.utc) diff --git a/tests/test_saved_filters.py b/tests/test_saved_filters.py index f81a787..d766a4c 100644 --- a/tests/test_saved_filters.py +++ b/tests/test_saved_filters.py @@ -1,7 +1,7 @@ import pytest -from thoughtsync.app import create_app -from thoughtsync.saved_filters import clean_params +from inkwell.app import create_app +from inkwell.saved_filters import clean_params @pytest.fixture diff --git a/tests/test_security.py b/tests/test_security.py index dafae41..ec733c2 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -1,4 +1,4 @@ -from thoughtsync.security import generate_token, hash_password, hash_token, verify_password +from inkwell.security import generate_token, hash_password, hash_token, verify_password def test_password_roundtrip(): diff --git a/tests/test_security_headers.py b/tests/test_security_headers.py index 9009673..938ebcb 100644 --- a/tests/test_security_headers.py +++ b/tests/test_security_headers.py @@ -6,7 +6,7 @@ hook, and this suite has no Postgres. """ import pytest -from thoughtsync.app import create_app +from inkwell.app import create_app @pytest.fixture diff --git a/tests/test_settings.py b/tests/test_settings.py index 4dd9b8c..7ce8cef 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,4 +1,4 @@ -from thoughtsync.settings import REGISTRY, validate_updates +from inkwell.settings import REGISTRY, validate_updates def test_registry_has_expected_keys(): diff --git a/tests/test_sync.py b/tests/test_sync.py index 51ee4e3..aed688f 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -2,8 +2,8 @@ from datetime import datetime, timezone import pytest -from thoughtsync.app import create_app -from thoughtsync.sync import ( +from inkwell.app import create_app +from inkwell.sync import ( DEFAULT_LIMIT, MAX_LIMIT, MIN_CLIENT_PROTOCOL_VERSION, diff --git a/tests/test_unfurl.py b/tests/test_unfurl.py index a5d844d..333e081 100644 --- a/tests/test_unfurl.py +++ b/tests/test_unfurl.py @@ -2,8 +2,8 @@ import ipaddress import pytest -from thoughtsync.app import create_app -from thoughtsync.unfurl import UnfurlError, extract_preview, is_public_ip, validate_url +from inkwell.app import create_app +from inkwell.unfurl import UnfurlError, extract_preview, is_public_ip, validate_url @pytest.fixture