Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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.
Expand Down
37 changes: 37 additions & 0 deletions desktop/scripts/install-main.sh
Original file line number Diff line number Diff line change
@@ -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
75 changes: 75 additions & 0 deletions desktop/scripts/make-app-icon-svg.py
Original file line number Diff line number Diff line change
@@ -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'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {SIZE} {SIZE}" width="{SIZE}" height="{SIZE}">
<!-- cot. app icon: macOS grid (824 tile inset 100, r185), Newsreader Bold Italic at opsz 72, outlined. -->
<defs>
<filter id="shadow" x="-10%" y="-10%" width="120%" height="125%">
<feDropShadow dx="0" dy="10" stdDeviation="12" flood-color="#000" flood-opacity="0.3"/>
</filter>
</defs>
<rect x="{INSET}" y="{INSET}" width="{TILE}" height="{TILE}" rx="{RADIUS}" fill="#0F5B3E" filter="url(#shadow)"/>
<path fill="#FFFFFF" d="{word}"/>
<path fill="#C8F169" d="{dot}"/>
</svg>
'''
OUT.write_text(svg)
print("wrote", OUT)
31 changes: 31 additions & 0 deletions desktop/scripts/make-app-icon.sh
Original file line number Diff line number Diff line change
@@ -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
Binary file modified desktop/src-tauri/icons/128x128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/128x128@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/64x64.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified desktop/src-tauri/icons/icon.icns
Binary file not shown.
Binary file modified desktop/src-tauri/icons/icon.ico
Binary file not shown.
Binary file modified desktop/src-tauri/icons/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions desktop/src-tauri/icons/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 61 additions & 0 deletions scripts/fresh-install.sh
Original file line number Diff line number Diff line change
@@ -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'
Loading