diff --git a/Justfile b/Justfile index 22aaf7c..44cd273 100644 --- a/Justfile +++ b/Justfile @@ -240,6 +240,23 @@ mac action="build": *) printf '%s\n' "unknown action: {{action}} (build|app|run|clean)" >&2; exit 2 ;; esac +# Fresh web install in a sandbox: runs `curl cot.run/install | sh` with HOME in a +# temp dir, so the collector container, ~/.cot and hook files never touch yours. +# just fresh collector built from origin/main +# just fresh release the published image users pull today +# just fresh down remove the container and sandbox +# COT_FRESH_HISTORY=1 imports your real transcripts read-only. +# Fresh web install in a sandbox: up | release | down +fresh action="up": + #!/usr/bin/env sh + set -eu + case "{{action}}" in + up) scripts/fresh-install.sh ;; + release) scripts/fresh-install.sh --release ;; + down) scripts/fresh-install.sh down ;; + *) printf '%s\n' "unknown action: {{action}} (up|release|down)" >&2; exit 2 ;; + esac + # Desktop app (Tauri): cot.app that runs the frozen collector, self-updates. # just desktop dev run the shell in dev mode (stages the collector once) # just desktop build build cot.app (dashboard + frozen collector + shell) @@ -248,9 +265,10 @@ mac action="build": # just desktop verify check the built app's collector answers /health # just desktop install install the built app to /Applications (old one kept in ~/cot-app-backups) # just desktop deploy build, verify, install +# just desktop prod install origin/main like a release: clean build, install, bridge + hooks # just desktop test Rust unit tests # just desktop clean drop build output and staged resources -# Desktop app: dev | build | shell | dmg | verify | install | deploy | test | clean +# Desktop app: dev | build | shell | dmg | verify | install | deploy | prod | test | clean desktop action="build": #!/usr/bin/env sh set -eu @@ -264,9 +282,10 @@ desktop action="build": verify) scripts/verify-bundle.sh ;; install) scripts/install.sh ;; deploy) scripts/build.sh && scripts/verify-bundle.sh && scripts/install.sh ;; + prod) scripts/install-main.sh ;; test) cargo test --manifest-path src-tauri/Cargo.toml ;; clean) rm -rf build src-tauri/target src-tauri/resources && printf '%s\n' "removed desktop build output" ;; - *) printf '%s\n' "unknown action: {{action}} (dev|build|shell|dmg|verify|install|deploy|test|clean)" >&2; exit 2 ;; + *) printf '%s\n' "unknown action: {{action}} (dev|build|shell|dmg|verify|install|deploy|prod|test|clean)" >&2; exit 2 ;; esac # Cut a release locally: bump backend/app/__init__.py, test, commit, annotated tag. diff --git a/desktop/scripts/install-main.sh b/desktop/scripts/install-main.sh new file mode 100755 index 0000000..a5621e2 --- /dev/null +++ b/desktop/scripts/install-main.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Install what's on origin/main the way a release would land on this Mac: +# build cot.app from a clean checkout of main, install it, then refresh the +# bridge and agent hooks from the new collector (what "Repair Agent Hooks…" runs). +# +# scripts/install-main.sh +# +# Builds in a dedicated worktree next to the main checkout (../cot-release, or +# COT_RELEASE_DIR), reset to origin/main each run, so branches and uncommitted +# work in your own checkouts never reach the build. Ignored caches (node_modules, +# desktop/build/venv) are kept between runs. The app is unsigned, unlike CI's. +set -euo pipefail + +REPO="$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --path-format=absolute --git-common-dir)" +RELEASE_DIR="${COT_RELEASE_DIR:-$(dirname "$(dirname "${REPO}")")/cot-release}" +ENDPOINT="${COT_ENDPOINT:-http://127.0.0.1:31337}" + +step() { printf '\n\033[1m› %s\033[0m\n' "$1"; } + +step "Fetching origin/main" +git --git-dir="${REPO}" fetch origin main + +if [ ! -d "${RELEASE_DIR}" ]; then + step "Creating release worktree at ${RELEASE_DIR}" + git --git-dir="${REPO}" worktree add --detach "${RELEASE_DIR}" origin/main +fi +cd "${RELEASE_DIR}" +git checkout -q --detach origin/main +git reset -q --hard origin/main +git clean -fdq +printf 'building %s\n' "$(git log -1 --format='%h %s')" + +# main's own recipes, so the build matches the commit being installed. +just desktop deploy + +step "Refreshing the bridge and agent hooks" +curl -fsSL "${ENDPOINT}/install.sh" | sh -s -- --repair diff --git a/desktop/scripts/make-app-icon-svg.py b/desktop/scripts/make-app-icon-svg.py new file mode 100644 index 0000000..493830c --- /dev/null +++ b/desktop/scripts/make-app-icon-svg.py @@ -0,0 +1,75 @@ +"""Outline the "cot." wordmark into src-tauri/icons/icon.svg, the app icon source. + +Newsreader Bold Italic at opsz 72 (what the app's wordmark renders at display +sizes), shaped with HarfBuzz so kerning matches the browser, on the macOS icon +grid: an 824px Forest tile inset 100px on a 1024 canvas, r185, soft drop shadow. +Run from desktop/ (needs network for the font): + uv run --with fonttools --with brotli --with uharfbuzz python scripts/make-app-icon-svg.py +Then scripts/make-app-icon.sh renders every size from the SVG. +""" +import io, sys, tempfile, urllib.request +from pathlib import Path +from fontTools.ttLib import TTFont +from fontTools.varLib import instancer +from fontTools.pens.svgPathPen import SVGPathPen +from fontTools.pens.boundsPen import BoundsPen +from fontTools.pens.transformPen import TransformPen +import uharfbuzz as hb + +FONT_URL = "https://fonts.gstatic.com/s/newsreader/v26/cY9XfjOCX1hbuyalUrK439vogqCz_goCYw7oRTmOFYYzbARA_n8.woff2" +OUT = Path(__file__).resolve().parent.parent / "src-tauri" / "icons" / "icon.svg" +with tempfile.TemporaryDirectory() as tmp: + woff2 = Path(tmp) / "newsreader-bold-italic.woff2" + urllib.request.urlretrieve(FONT_URL, woff2) + font = TTFont(woff2) + font.ensureDecompiled() +if "fvar" in font: + font = instancer.instantiateVariableFont(font, {a.axisTag: (72 if a.axisTag == "opsz" else 700 if a.axisTag == "wght" else a.defaultValue) for a in font["fvar"].axes}) +font.flavor = None +buf = io.BytesIO(); font.save(buf); data = buf.getvalue() +upem = font["head"].unitsPerEm +gs = font.getGlyphSet() + +hbfont = hb.Font(hb.Face(data)) +b = hb.Buffer(); b.add_str("cot."); b.guess_segment_properties() +hb.shape(hbfont, b, {"kern": True, "liga": True}) +order = font.getGlyphOrder() + +# Lay glyphs out in font units (y up), record per-glyph paths. +x = 0; glyphs = [] +for info, pos in zip(b.glyph_infos, b.glyph_positions): + glyphs.append((order[info.codepoint], x + pos.x_offset, pos.y_offset)); x += pos.x_advance + +SIZE, INSET, TILE, RADIUS = 1024, 100, 824, 185 +FONT_PX = 340 +scale = FONT_PX / upem +# Ink bounds of the whole word. +bp = BoundsPen(gs) +for name, gx, gy in glyphs: + gs[name].draw(TransformPen(bp, (1, 0, 0, 1, gx, gy))) +xmin, ymin, xmax, ymax = bp.bounds +cx, cy = (xmin + xmax) / 2, (ymin + ymax) / 2 +# Map font units -> canvas: flip y, center ink on canvas. +def tf(gx, gy): return (scale, 0, 0, -scale, SIZE / 2 + (gx - cx) * scale, SIZE / 2 + (cy - gy) * scale) +paths = [] +for name, gx, gy in glyphs: + pen = SVGPathPen(gs) + gs[name].draw(TransformPen(pen, tf(gx, gy))) + paths.append((name, pen.getCommands())) + +word = " ".join(d for n, d in paths if n != "period") +dot = " ".join(d for n, d in paths if n == "period") +svg = f''' + + + + + + + + + + +''' +OUT.write_text(svg) +print("wrote", OUT) diff --git a/desktop/scripts/make-app-icon.sh b/desktop/scripts/make-app-icon.sh new file mode 100755 index 0000000..40c7261 --- /dev/null +++ b/desktop/scripts/make-app-icon.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Render src-tauri/icons/icon.svg into every app icon Tauri bundles: the PNGs, +# icon.icns (macOS) and icon.ico (Windows). Needs rsvg-convert and ImageMagick +# (brew install librsvg imagemagick). Edit the SVG with make-app-icon-svg.py. +set -euo pipefail + +ICONS="$(cd "$(dirname "${BASH_SOURCE[0]}")/../src-tauri/icons" && pwd)" +SVG="${ICONS}/icon.svg" +TMP="$(mktemp -d)" +trap 'rm -rf "${TMP}"' EXIT + +render() { rsvg-convert -w "$1" -h "$1" "${SVG}" -o "$2"; } + +render 32 "${ICONS}/32x32.png" +render 64 "${ICONS}/64x64.png" +render 128 "${ICONS}/128x128.png" +render 256 "${ICONS}/128x128@2x.png" +render 512 "${ICONS}/icon.png" + +SET="${TMP}/icon.iconset" +mkdir "${SET}" +for size in 16 32 128 256 512; do + render "${size}" "${SET}/icon_${size}x${size}.png" + render "$((size * 2))" "${SET}/icon_${size}x${size}@2x.png" +done +iconutil -c icns "${SET}" -o "${ICONS}/icon.icns" + +for size in 16 24 32 48 64 256; do render "${size}" "${TMP}/ico-${size}.png"; done +magick "${TMP}"/ico-{16,24,32,48,64,256}.png "${ICONS}/icon.ico" + +ls -la "${ICONS}"/*.png "${ICONS}"/icon.icns "${ICONS}"/icon.ico diff --git a/desktop/src-tauri/icons/128x128.png b/desktop/src-tauri/icons/128x128.png index f15f228..01ac358 100644 Binary files a/desktop/src-tauri/icons/128x128.png and b/desktop/src-tauri/icons/128x128.png differ diff --git a/desktop/src-tauri/icons/128x128@2x.png b/desktop/src-tauri/icons/128x128@2x.png index 4965c40..c8185df 100644 Binary files a/desktop/src-tauri/icons/128x128@2x.png and b/desktop/src-tauri/icons/128x128@2x.png differ diff --git a/desktop/src-tauri/icons/32x32.png b/desktop/src-tauri/icons/32x32.png index 5e9e903..2a714dd 100644 Binary files a/desktop/src-tauri/icons/32x32.png and b/desktop/src-tauri/icons/32x32.png differ diff --git a/desktop/src-tauri/icons/64x64.png b/desktop/src-tauri/icons/64x64.png index 2917736..bd4feb3 100644 Binary files a/desktop/src-tauri/icons/64x64.png and b/desktop/src-tauri/icons/64x64.png differ diff --git a/desktop/src-tauri/icons/icon.icns b/desktop/src-tauri/icons/icon.icns index cf1996e..7105242 100644 Binary files a/desktop/src-tauri/icons/icon.icns and b/desktop/src-tauri/icons/icon.icns differ diff --git a/desktop/src-tauri/icons/icon.ico b/desktop/src-tauri/icons/icon.ico index ac81b44..5973e68 100644 Binary files a/desktop/src-tauri/icons/icon.ico and b/desktop/src-tauri/icons/icon.ico differ diff --git a/desktop/src-tauri/icons/icon.png b/desktop/src-tauri/icons/icon.png index 9be3fec..9b52bc3 100644 Binary files a/desktop/src-tauri/icons/icon.png and b/desktop/src-tauri/icons/icon.png differ diff --git a/desktop/src-tauri/icons/icon.svg b/desktop/src-tauri/icons/icon.svg new file mode 100644 index 0000000..22397ba --- /dev/null +++ b/desktop/src-tauri/icons/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/scripts/fresh-install.sh b/scripts/fresh-install.sh new file mode 100755 index 0000000..e47d03f --- /dev/null +++ b/scripts/fresh-install.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Try the web install the way a new user gets it, without touching your own +# cot: `curl -fsSL https://cot.run/install | sh` runs with HOME pointed at a +# throwaway directory, so the collector container, ~/.cot (database, bridge, +# config) and the agent hook files all land in the sandbox. +# +# scripts/fresh-install.sh collector image built from origin/main +# scripts/fresh-install.sh --release the published image (what users pull today) +# scripts/fresh-install.sh down remove the container and the sandbox +# +# COT_FRESH_HISTORY=1 import your real Claude/Cursor/Codex transcripts (read only) +# COT_FRESH_PORT=31390 collector port; COT_FRESH_DIR sandbox path +# +# Your agents still read your real hook files, so live sessions keep going to +# your own collector; the sandbox shows onboarding, import and the dashboard. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SANDBOX="${COT_FRESH_DIR:-${TMPDIR:-/tmp}/cot-fresh}" +PORT="${COT_FRESH_PORT:-31390}" +CONTAINER="cot-fresh" +LOCAL_IMAGE="cot-fresh:main" +RELEASE_IMAGE="ghcr.io/cot-intelligence/cot:latest" + +step() { printf '\n\033[1m› %s\033[0m\n' "$1"; } + +down() { + docker rm -f "${CONTAINER}" >/dev/null 2>&1 || true + rm -rf "${SANDBOX}" + printf 'removed %s and %s\n' "${CONTAINER}" "${SANDBOX}" +} + +case "${1:-}" in + down) down; exit 0 ;; + --release) image="${RELEASE_IMAGE}" ;; + "") + step "Building the collector image from origin/main" + git -C "${REPO_ROOT}" fetch origin main + git -C "${REPO_ROOT}" archive origin/main | docker build -q -f Dockerfile.app -t "${LOCAL_IMAGE}" - + image="${LOCAL_IMAGE}" + ;; + *) echo "usage: $0 [--release|down]" >&2; exit 2 ;; +esac + +step "Resetting the sandbox at ${SANDBOX}" +down +mkdir -p "${SANDBOX}/home" + +if [ "${COT_FRESH_HISTORY:-0}" = 1 ]; then + # Transcript roots only; hooks and ~/.cot still go to the sandbox HOME. + export COT_CLAUDE_HOME="${HOME}/.claude" COT_CURSOR_HOME="${HOME}/.cursor" COT_CODEX_HOME="${HOME}/.codex" +fi + +step "Running the cot.run installer as a new user" +curl -fsSL https://cot.run/install \ + | HOME="${SANDBOX}/home" COT_CONTAINER="${CONTAINER}" COT_IMAGE="${image}" COT_PORT="${PORT}" sh + +printf '\n\033[1mFresh install running\033[0m\n' +printf ' dashboard %s\n' "$(cat "${SANDBOX}/home/.cot/config.json" 2>/dev/null | sed -n 's/.*"endpoint"[^"]*"\([^"]*\)".*/\1/p' | head -n1)" +printf ' sandbox %s\n' "${SANDBOX}/home" +printf ' remove just fresh down\n'