git.lucas.co / gitsite
the static git browser that builds this site
git clone https://git.lucas.co/gitsite.git

commit672a9be95b561b198ec2d59b083aefece6a42978
parent827bc4eca9
authorLucas Galante <[email protected]>
date2026-09-20 13:53
Mirror from GitHub; retire the ~/git bare layer

GitHub is canonical now: every work tree's origin is
https://github.com/lsgalante/<name>.git and this site mirrors from there.
repos.conf carries the GitHub URLs, autodeploy.sh asks GitHub for each
HEAD with ls-remote instead of reading a local bare repo, and build.sh
re-points an existing mirror's origin at whatever repos.conf says so the
cached mirrors follow the move without a re-clone.

git-bare-sync.sh goes with the bare layer it maintained; the readme's
publishing model is rewritten and keeps the history, including the three
repos whose hashes changed on the way to GitHub.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

 autodeploy.sh    |   4 +-
 build.sh         |   4 +
 git-bare-sync.sh | 242 -------------------------------------------------------
 readme.txt       |  79 +++++++-----------
 repos.conf       |  77 +++++++++---------
 5 files changed, 76 insertions(+), 330 deletions(-)

diff --git a/autodeploy.sh b/autodeploy.sh
index abb6829..7fdb61d 100755
--- a/autodeploy.sh
+++ b/autodeploy.sh
@@ -8,7 +8,9 @@ STATE="$HOME/.cache/gitsite/last-publish-heads"
 
 current=$(grep -v '^#' "$BASE/repos.conf" | grep -v '^$' | \
     while IFS='|' read -r name path desc mode; do
-        echo "$name $(git -C "$path" rev-parse HEAD 2>/dev/null || echo empty)"
+        # path is a URL (GitHub); ls-remote is one round trip per repo. A
+        # repo with no commits yet answers nothing, hence "empty".
+        echo "$name $(git ls-remote "$path" HEAD 2>/dev/null | cut -f1 | grep . || echo empty)"
     done | sha256sum | cut -d' ' -f1)
 
 if [ -f "$STATE" ] && [ "$(cat "$STATE")" = "$current" ]; then
diff --git a/build.sh b/build.sh
index 6152124..721f8d1 100755
--- a/build.sh
+++ b/build.sh
@@ -16,6 +16,10 @@ grep -v '^#' "$BASE/repos.conf" | grep -v '^$' | while IFS='|' read -r name path
         # small packfiles: Cloudflare Pages rejects files over 25MB
         git -C "$m" repack -a -d -q --max-pack-size=20m
     else
+        # repos.conf is the source of truth for where a repo lives; a mirror
+        # made when it pointed elsewhere (the ~/git bare layer, before
+        # 2026-09-20) follows it here rather than fetching from a stale path.
+        git -C "$m" remote set-url origin "$path"
         git -C "$m" fetch --quiet --prune origin
     fi
 done
diff --git a/git-bare-sync.sh b/git-bare-sync.sh
deleted file mode 100755
index d290295..0000000
--- a/git-bare-sync.sh
+++ /dev/null
@@ -1,242 +0,0 @@
-#!/bin/sh
-# Create and maintain the local bare-repo layer under ~/git.
-#
-#   ~/git/<name>.git          bare, pushable -- the canonical copy
-#         ^ git push
-#   working tree              gitsite mirrors from the bare repo -> git.lucas.co
-#
-# Why this exists: nothing in the setup could be pushed to. The cce repos'
-# origin was https://git.lucas.co/<name>.git, which is static dumb-http -- you
-# can clone from it, you cannot push to it, because there is no receive-pack
-# behind static hosting. So "published" meant "a timer happened to run", with
-# no signal either way. gitsite's own readme names the hazard: if the timer
-# stops, local commits look published but aren't.
-#
-# With a bare remote, `git push` succeeds or fails now, and the bare repo is a
-# second real copy on disk -- independent of the working tree, so an rm -rf no
-# longer costs everything since the last nightly restic run.
-#
-# Safe to re-run. Existing bare repos are fetched into, not recreated, and an
-# existing origin is preserved under a descriptive name rather than dropped.
-#
-# Which repos: every line of repos.conf, by NAME (field 1). Field 2 is the BARE
-# path -- what gitsite reads HEAD from -- not a work tree, so work trees are
-# resolved by name under $WORK_ROOTS. A name that resolves to nothing is
-# reported and fails the run; it is never silently skipped.
-#
-# Run it only on the machine that owns the bare layer. The bare repos are
-# canonical and live on exactly one host; on any other machine a work tree's
-# origin is a URL pointing back at that host over the network, and this script
-# -- which computes bare="$BARE_ROOT/$name.git" and rewrites origin to that
-# local path -- would hand the second machine its own divergent bare repos and
-# file the real remote away under "previous". It would then look like it was
-# publishing while pushing to its own disk, which is the failure this whole
-# arrangement exists to prevent. A remote-looking origin is therefore refused.
-# On a second machine just use git: `git push origin <branch>`.
-#
-# GIT_BARE_HOST=1 overrides the refusal. That is the migration case this script
-# was written for, where an old fetch-only origin (git.lucas.co, github) is
-# deliberately replaced by a local bare repo.
-#
-# Usage:  git-bare-sync.sh [--dry-run]
-#   env:  GIT_BARE_ROOT (default ~/git), GIT_WORK_ROOTS, REPOS_CONF,
-#         GIT_BARE_HOST
-
-set -eu
-
-BARE_ROOT="${GIT_BARE_ROOT:-$HOME/git}"
-REPOS_CONF="${REPOS_CONF:-$HOME/projects/gitsite/repos.conf}"
-DRY=""
-[ "${1:-}" = "--dry-run" ] && DRY=1
-
-# Where work trees live. repos.conf does NOT record them -- its field 2 is the
-# BARE path, because that is what gitsite reads HEAD from -- so a repo's work
-# tree is found by NAME under these roots, first match winning.
-WORK_ROOTS="${GIT_WORK_ROOTS:-$HOME/projects/cce $HOME/projects $HOME/Dropbox/src}"
-
-# Repos that are not published through gitsite and so are absent from
-# repos.conf. Add a line here (a work-tree path), or add them to repos.conf to
-# publish them too. Empty now that hou-control and gitsite are both listed
-# there -- they were carried here with a path that had since gone stale.
-EXTRA_REPOS=""
-
-# The work tree for a repo name, or nothing if it is not under WORK_ROOTS.
-find_worktree() {
-    _name=$1
-    for _root in $WORK_ROOTS; do
-        if [ -d "$_root/$_name/.git" ]; then
-            printf '%s\n' "$_root/$_name"
-            return 0
-        fi
-    done
-    return 1
-}
-
-say() { printf '%s\n' "$*"; }
-run() {
-    if [ -n "$DRY" ]; then
-        printf '    would: %s\n' "$*"
-    else
-        "$@"
-    fi
-}
-
-# Does this origin point at a bare layer on another machine? A local bare repo
-# is an absolute path; a URL scheme or an scp-style host:path is somewhere
-# else. file:// is a URL but still local, so it is not remote.
-is_remote_url() {
-    case "$1" in
-        ''|/*|./*|../*|~*) return 1 ;;
-        file://*)          return 1 ;;
-        *://*)             return 0 ;;
-        *:*)               return 0 ;;
-        *)                 return 1 ;;
-    esac
-}
-
-# A name for an existing origin, so replacing it loses no information.
-preserved_name() {
-    case "$1" in
-        *git.lucas.co*) echo "published" ;;   # static mirror: fetch-only
-        *codeberg.org*) echo "codeberg"  ;;   # no longer used
-        *github.com*)   echo "github"    ;;
-        *)              echo "previous"  ;;
-    esac
-}
-
-# Runs in a subshell so a failure inside it is returnable rather than fatal
-# under `set -e`. fail() records the first error and keeps going, so a repo
-# that cannot be cloned still gets reported rather than silently skipped.
-sync_one() (
-    set +e
-    rc=0
-    fail() { rc=1; }
-    path=$1
-    [ -d "$path/.git" ] || { say "  skip $path (not a git work tree)"; exit 0; }
-    name=$(basename "$path")
-    bare="$BARE_ROOT/$name.git"
-    current=$(git -C "$path" remote get-url origin 2>/dev/null || true)
-
-    # Client-mode guard, before anything is written: creating the bare repo is
-    # itself a mutation, so the origin has to be inspected first.
-    if [ -z "${GIT_BARE_HOST:-}" ] && is_remote_url "$current"; then
-        say "  !! $name: origin is $current"
-        say "     That is a bare layer on another machine. Running here would"
-        say "     replace it with $bare, giving this machine its own divergent"
-        say "     bare repos while the real remote is filed away as 'previous'."
-        say "     Push from here with git directly: git push origin <branch>."
-        say "     Run this only on the host that owns $BARE_ROOT."
-        say "     GIT_BARE_HOST=1 overrides, for replacing a legacy origin."
-        fail
-        exit $rc
-    fi
-
-    if [ -d "$bare" ]; then
-        say "  $name: bare exists"
-    else
-        say "  $name: creating $bare"
-        run git clone --bare --quiet "$path" "$bare" || { fail; exit $rc; }
-        # clone --bare leaves a back-reference to the work tree; the bare repo
-        # is the upstream, so it should not point anywhere.
-        run git -C "$bare" remote remove origin 2>/dev/null || true
-        run git -C "$bare" config gc.auto 6700
-    fi
-
-    if [ -n "$current" ]; then
-        case "$current" in
-            "$bare") : ;;   # already pointed at the bare repo
-            *)
-                keep=$(preserved_name "$current")
-                if git -C "$path" remote get-url "$keep" >/dev/null 2>&1; then
-                    run git -C "$path" remote remove origin
-                else
-                    say "    keeping old origin as '$keep' ($current)"
-                    run git -C "$path" remote rename origin "$keep"
-                fi
-                run git -C "$path" remote add origin "$bare"
-                ;;
-        esac
-    else
-        run git -C "$path" remote add origin "$bare"
-    fi
-
-    # A repo with no commits yet has nothing to push and is not a failure.
-    if ! git -C "$path" rev-parse --quiet --verify HEAD >/dev/null 2>&1; then
-        say "    no commits yet -- bare repo created, nothing to push"
-        exit 0
-    fi
-
-    # Push every branch and tag. --all covers main/master without caring which.
-    run git -C "$path" push --quiet origin --all || fail
-    run git -C "$path" push --quiet origin --tags || fail
-
-    branch=$(git -C "$path" symbolic-ref --short HEAD 2>/dev/null || true)
-    if [ -n "$branch" ]; then
-        run git -C "$path" branch --quiet --set-upstream-to="origin/$branch" "$branch" 2>/dev/null || true
-        # Point the bare repo's HEAD at the same branch, so cloning it checks
-        # out the right thing and gitsite shows the right default.
-        run git -C "$bare" symbolic-ref HEAD "refs/heads/$branch" || fail
-    fi
-    exit $rc
-)
-
-[ -n "$DRY" ] && say "DRY RUN -- nothing will be written"
-run mkdir -p "$BARE_ROOT"
-
-FAILED=""
-
-# One damaged repo must not abort the other thirty-three. A repo whose refs
-# Dropbox has mangled makes git exit non-zero here; record it and carry on.
-attempt() {
-    if sync_one "$1"; then
-        :
-    else
-        say "  !! $(basename "$1") FAILED -- see above"
-        FAILED="$FAILED $(basename "$1")"
-    fi
-}
-
-say "from $REPOS_CONF:"
-# Read the whole list first: the loop body runs in this shell, not a subshell,
-# so a failure is visible to the caller rather than swallowed by a pipeline.
-# Fields 1|2 are name|bare-path (no spaces around the separators, per the file's
-# own header). This used to iterate field 2 and hand it to sync_one as though it
-# were a work tree; every repo then reported "not a git work tree" and was
-# skipped, so nothing was pushed for weeks while commits piled up looking
-# published. Hence: resolve by NAME, and treat an unresolvable one as a failure
-# rather than a skip.
-list=$(grep -v '^#' "$REPOS_CONF" | grep -v '^[[:space:]]*$' | cut -d'|' -f1,2)
-for entry in $list; do
-    name=${entry%%|*}
-    conf_bare=${entry#*|}
-    if ! path=$(find_worktree "$name"); then
-        say "  !! $name: no work tree under $WORK_ROOTS -- NOT synced"
-        FAILED="$FAILED $name"
-        continue
-    fi
-    # gitsite publishes whatever field 2 points at, while this script maintains
-    # $BARE_ROOT/<name>.git. If the two ever diverge, pushes land in a repo the
-    # site never reads -- silent non-publishing again, from the other end.
-    if [ "$BARE_ROOT" = "$HOME/git" ] && [ "$conf_bare" != "$BARE_ROOT/$name.git" ]; then
-        say "  !! $name: repos.conf publishes $conf_bare, but this syncs"
-        say "     $BARE_ROOT/$name.git -- pushes would not reach the site"
-        FAILED="$FAILED $name"
-        continue
-    fi
-    attempt "$path"
-done
-
-if [ -n "$EXTRA_REPOS" ]; then
-    say "extras:"
-    for path in $EXTRA_REPOS; do
-        attempt "$path"
-    done
-fi
-
-say ""
-if [ -n "$FAILED" ]; then
-    say "done, with failures:$FAILED"
-    say "bare repos in $BARE_ROOT"
-    exit 1
-fi
-say "done. bare repos in $BARE_ROOT"
diff --git a/readme.txt b/readme.txt
index 9c6c93c..a311cd3 100644
--- a/readme.txt
+++ b/readme.txt
@@ -9,12 +9,9 @@ repos.conf, plus dumb-http clone support so
 Files
 -----
 repos.conf   which repos to publish: name|path|description|mode
-             path is the BARE repo in ~/git -- what this reads HEAD from,
-             not a work tree
+             path is the GitHub URL the repo is mirrored from -- what
+             this reads HEAD from, not a work tree
              mode "clone" = browse + clonable, "browse" = browse only
-git-bare-sync.sh
-             creates/maintains the ~/git bare repos and pushes every work
-             tree into them -- see "Publishing model" below
 generate.py  the HTML generator
 build.sh     syncs mirrors, runs generate.py, adds clone files
 deploy.sh    pushes the built site to Cloudflare Pages
@@ -52,53 +49,36 @@ Log:    ~/.cache/gitsite/autodeploy.log
 
 Publishing model
 ----------------
-Committing is NOT publishing; pushing is. Each repo's origin is a local
-bare repo under ~/git (real, pushable, and a second copy on disk); this
-site mirrors from those, and repos.conf lists them. So:
+Committing is NOT publishing; pushing is. Each repo's origin is
+https://github.com/lsgalante/<name>.git, GitHub is canonical, and this
+site is a mirror of it: repos.conf lists the GitHub URLs, build.sh keeps a
+bare mirror of each in ~/.cache/gitsite/mirrors, and autodeploy.sh asks
+GitHub (git ls-remote) whether any HEAD moved. So:
 
     git commit ...              # local only
-    git push origin <branch>    # the publishing step
+    git push                    # the publishing step
                                 # gitsite.timer then deploys it
 
-That replaced an older arrangement where origin was this site itself --
-fetch-only, static, no receive-pack -- and "published" meant "a timer
-happened to run", with no signal either way.
-
-git-bare-sync.sh creates any missing bare repos and pushes every repo in
-repos.conf into its own, so it is the bulk version of that push step:
-
-    git-bare-sync.sh --dry-run   # show what would be pushed
-    git-bare-sync.sh             # do it
-
-It finds each repo's work tree by NAME under ~/projects/cce, ~/projects
-and ~/Dropbox/src (override with GIT_WORK_ROOTS), because repos.conf
-records only the bare path. A name it cannot resolve fails the run rather
-than being skipped: it previously read repos.conf's bare path AS a work
-tree, found no .git, and silently skipped all 34 repos -- which left 21
-of them holding unpushed commits (2026-09-18) while the docs still said a
-commit was enough.
-
-Run it only on the machine that owns ~/git. The bare repos are canonical
-and live on exactly one host; on a second machine a work tree's origin is
-a URL pointing back here over the network, and this script -- which
-rewrites origin to $BARE_ROOT/<name>.git -- would hand that machine its
-own divergent bare layer and file the real remote away under "previous".
-It would then look like it was publishing while pushing to its own disk,
-which is the failure the bare layer was introduced to eliminate. So a
-remote-looking origin (a URL scheme, or scp-style host:path) is refused,
-and refused before anything is created, since creating a bare repo is
-already a mutation. On a second machine, just use git:
-
-    git push origin <branch>    # same publishing step, no bulk tool
-
-GIT_BARE_HOST=1 overrides the refusal. That is the migration case this
-script was written for: an old fetch-only origin deliberately replaced by
-a local bare repo.
-
-It lives here rather than loose in ~/.local/bin, where it was unversioned
-and one rm from gone; ~/.local/bin/git-bare-sync.sh is a symlink to this
-copy, so PATH and the docs that name that path keep working and there is
-only one file to edit.
+The 27 crates pinned to https://git.lucas.co/<name>.git?rev=... keep
+working because the dumb-http clone dirs are built from the same mirrors;
+a rev exists here as long as it is reachable on GitHub.
+
+History, for when something looks odd:
+
+- Until 2026-09-18 each repo's origin was this site itself: fetch-only,
+  static, no receive-pack, so "published" meant "a timer happened to run".
+- 2026-09-18..20 a local bare-repo layer under ~/git was the origin and
+  this site mirrored from it (git-bare-sync.sh, now deleted). A Forgejo
+  instance at forge.lucas.co briefly mirrored the same layer.
+- 2026-09-20 everything moved to GitHub. Three repos were rewritten on the
+  way, so their hashes before that date do not match the ones here:
+  cce-fonts and cce-gallery had a committed target/ directory (blobs up to
+  403MB; GitHub refuses anything over 100MB), and cce-system-interface had
+  a Google OAuth client ID and secret compiled into
+  src/pages/accounts.rs. Both values are scrubbed from every commit; the
+  secret was public here and had to be rotated regardless.
+- hou-control already existed on GitHub with an unrelated 2024-2025
+  history; that is preserved there as branch main-2024.
 
 One-time Cloudflare setup
 -------------------------
@@ -133,4 +113,5 @@ Notes
 - Mirrors live in ~/.cache/gitsite/mirrors, repacked into <=20MB packs
   (Cloudflare rejects files over 25MB). Delete a mirror dir to force a
   fresh re-mirror.
-- To add a repo: add a line to repos.conf, run build.sh + deploy.sh.
+- To add a repo: create it on GitHub (gh repo create lsgalante/<name>
+  --public), push, add a line to repos.conf, run build.sh + deploy.sh.
diff --git a/repos.conf b/repos.conf
index 1aa8d1b..c57e9df 100644
--- a/repos.conf
+++ b/repos.conf
@@ -1,41 +1,42 @@
 # name|path|description|mode
-#   path: the bare repo in ~/git -- publishing follows what was PUSHED,
-#         not merely committed. See git-bare-sync.sh.
+#   path: where the repo is fetched from. GitHub is canonical and this site
+#         is a mirror of it: publishing follows what was PUSHED to GitHub,
+#         not merely committed.
 # mode: "clone" = browsable + clonable via dumb http, "browse" = browsable only
 # no spaces around the | separators
-website|/home/lsgalante/git/website.git|source for lucas.co|browse
-hou-control|/home/lsgalante/git/hou-control.git|SideFX Houdini customization package|clone
-gitsite|/home/lsgalante/git/gitsite.git|the static git browser that builds this site|clone
-cce|/home/lsgalante/git/cce.git|cce workspace root: member list, lockfile and dependency pins|clone
-cce-authenticator|/home/lsgalante/git/cce-authenticator.git|login authentication (PAM + fingerprint)|clone
-cce-browser|/home/lsgalante/git/cce-browser.git|web browser (Servo)|clone
-cce-calendar|/home/lsgalante/git/cce-calendar.git|calendar|clone
-cce-cloud|/home/lsgalante/git/cce-cloud.git|cloud storage client|clone
-cce-color-editor|/home/lsgalante/git/cce-color-editor.git|color picker and palette editor|clone
-cce-compositor|/home/lsgalante/git/cce-compositor.git|Wayland compositor (wlroots)|clone
-cce-data-editor|/home/lsgalante/git/cce-data-editor.git|structured data editor|clone
-cce-designer|/home/lsgalante/git/cce-designer.git|graphic design tool|clone
-cce-display-manager|/home/lsgalante/git/cce-display-manager.git|login greeter|clone
-cce-files|/home/lsgalante/git/cce-files.git|file manager|clone
-cce-fonts|/home/lsgalante/git/cce-fonts.git|font browser|browse
-cce-graph|/home/lsgalante/git/cce-graph.git|node-based graph editor|clone
-cce-icons|/home/lsgalante/git/cce-icons.git|SVG icon set|clone
-cce-layout-interface|/home/lsgalante/git/cce-layout-interface.git|page layout tool|clone
-cce-lock|/home/lsgalante/git/cce-lock.git|session locker (ext-session-lock + PAM)|clone
-cce-mail|/home/lsgalante/git/cce-mail.git|mail client (IMAP/SMTP)|clone
-cce-grid|/home/lsgalante/git/cce-grid.git|desktop grid client|clone
-cce-map|/home/lsgalante/git/cce-map.git|map viewer|clone
-cce-notifier|/home/lsgalante/git/cce-notifier.git|notification daemon|clone
-cce-preview|/home/lsgalante/git/cce-preview.git|document and image viewer|clone
-cce-remote|/home/lsgalante/git/cce-remote.git|remote trackpad and keyboard server|clone
-cce-screenaver|/home/lsgalante/git/cce-screenaver.git|screensaver|clone
-cce-secrets|/home/lsgalante/git/cce-secrets.git|secrets manager|clone
-cce-status-interface|/home/lsgalante/git/cce-status-interface.git|status bar|clone
-cce-system-interface|/home/lsgalante/git/cce-system-interface.git|system settings|clone
-cce-terminal|/home/lsgalante/git/cce-terminal.git|terminal emulator|clone
-cce-gallery|/home/lsgalante/git/cce-gallery.git|widget gallery and compositor test bench|browse
-cce-text-editor|/home/lsgalante/git/cce-text-editor.git|text editor|clone
-cce-ui|/home/lsgalante/git/cce-ui.git|GPU-accelerated UI toolkit (Vulkan)|clone
-cce-window-manager|/home/lsgalante/git/cce-window-manager.git|window management library|clone
-clear-typeface|/home/lsgalante/git/clear-typeface.git|typeface (empty for now)|clone
-cce-list|/home/lsgalante/git/cce-list.git|things-to-remember checklist|clone
+website|https://github.com/lsgalante/website.git|source for lucas.co|browse
+hou-control|https://github.com/lsgalante/hou-control.git|SideFX Houdini customization package|clone
+gitsite|https://github.com/lsgalante/gitsite.git|the static git browser that builds this site|clone
+cce|https://github.com/lsgalante/cce.git|cce workspace root: member list, lockfile and dependency pins|clone
+cce-authenticator|https://github.com/lsgalante/cce-authenticator.git|login authentication (PAM + fingerprint)|clone
+cce-browser|https://github.com/lsgalante/cce-browser.git|web browser (Servo)|clone
+cce-calendar|https://github.com/lsgalante/cce-calendar.git|calendar|clone
+cce-cloud|https://github.com/lsgalante/cce-cloud.git|cloud storage client|clone
+cce-color-editor|https://github.com/lsgalante/cce-color-editor.git|color picker and palette editor|clone
+cce-compositor|https://github.com/lsgalante/cce-compositor.git|Wayland compositor (wlroots)|clone
+cce-data-editor|https://github.com/lsgalante/cce-data-editor.git|structured data editor|clone
+cce-designer|https://github.com/lsgalante/cce-designer.git|graphic design tool|clone
+cce-display-manager|https://github.com/lsgalante/cce-display-manager.git|login greeter|clone
+cce-files|https://github.com/lsgalante/cce-files.git|file manager|clone
+cce-fonts|https://github.com/lsgalante/cce-fonts.git|font browser|browse
+cce-graph|https://github.com/lsgalante/cce-graph.git|node-based graph editor|clone
+cce-icons|https://github.com/lsgalante/cce-icons.git|SVG icon set|clone
+cce-layout-interface|https://github.com/lsgalante/cce-layout-interface.git|page layout tool|clone
+cce-lock|https://github.com/lsgalante/cce-lock.git|session locker (ext-session-lock + PAM)|clone
+cce-mail|https://github.com/lsgalante/cce-mail.git|mail client (IMAP/SMTP)|clone
+cce-grid|https://github.com/lsgalante/cce-grid.git|desktop grid client|clone
+cce-map|https://github.com/lsgalante/cce-map.git|map viewer|clone
+cce-notifier|https://github.com/lsgalante/cce-notifier.git|notification daemon|clone
+cce-preview|https://github.com/lsgalante/cce-preview.git|document and image viewer|clone
+cce-remote|https://github.com/lsgalante/cce-remote.git|remote trackpad and keyboard server|clone
+cce-screenaver|https://github.com/lsgalante/cce-screenaver.git|screensaver|clone
+cce-secrets|https://github.com/lsgalante/cce-secrets.git|secrets manager|clone
+cce-status-interface|https://github.com/lsgalante/cce-status-interface.git|status bar|clone
+cce-system-interface|https://github.com/lsgalante/cce-system-interface.git|system settings|clone
+cce-terminal|https://github.com/lsgalante/cce-terminal.git|terminal emulator|clone
+cce-gallery|https://github.com/lsgalante/cce-gallery.git|widget gallery and compositor test bench|browse
+cce-text-editor|https://github.com/lsgalante/cce-text-editor.git|text editor|clone
+cce-ui|https://github.com/lsgalante/cce-ui.git|GPU-accelerated UI toolkit (Vulkan)|clone
+cce-window-manager|https://github.com/lsgalante/cce-window-manager.git|window management library|clone
+clear-typeface|https://github.com/lsgalante/clear-typeface.git|typeface (empty for now)|clone
+cce-list|https://github.com/lsgalante/cce-list.git|things-to-remember checklist|clone