Files
platform/scripts/setup/officer-setup/lib/layout.sh
T
pastilhasandClaude Opus 5 4b2c896523 ship the capabilities repository, and update it from the same button as plugins
capabilities/ is the file-based item store, and most of it belongs to the
owner: the six item directories are created by the platform at boot and
extensions/tool-loader is written from platform source, so neither is tracked
anywhere. What IS tracked is the small set the platform ships — today one
affordance, telling an agent that markdown images render and that the USER'S
DEVICE fetches the URL, which it cannot discover from the filesystem.

That now lives in officerdev/capabilities, public so a fresh install can clone
it without credentials.

Two ways in. The setup script syncs it during the layout step — non-fatal, and
it says what an install without it costs rather than failing: agents go back to
guessing, which is what they did before this existed. And "Update all" on the
marketplace screen carries it alongside the plugins, because it is the same
question — get me what has been published since I installed — and a second
different gesture for it is one nobody remembers.

Three states in both places, and the third is the normal one on an existing
machine: a checkout is pulled, an empty directory is cloned into, and a
directory that already holds the platform's generated items gets a repository
initialised around them. UNTRACKED files never block a pull, because the
owner's own skills and tools living here is the ordinary state — only tracked
changes do.

`cloned` and `adopted` are reported in the UI where `pulled` is not: they mean
this machine had no affordances at all until the button was pressed.

Verified against the real checkout: sync reports pulled, and the loader finds
officer/core/image-rendering afterwards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 07:34:30 +00:00

106 lines
4.4 KiB
Bash

#!/bin/bash
# =============================================================================
# officer-setup — the install layout
# =============================================================================
#
# Definitions only.
#
# ── One root, and nothing configurable underneath it ──
#
# $OFFICER_ROOT/
# platform/ the app — the git checkout
# data/ DATA_PATH: managed homes, attachments, job logs
# dockers/ services the app store provisioned
# capabilities/ the file-based item store — skills, tools, tasks, processes
#
# The original asked separately for DATA_PATH and for OFFICER_ITEMS_DIR, and left
# the app store's directory implicit. Three answers that had to agree with each
# other, given by somebody with no reason to know they had to.
#
# Now one question — where the root goes — and the rest follows. Anybody who wants
# data/ on a bigger volume can symlink it; that is a decision about storage, not
# about how Officer is laid out, and it does not need a prompt in a setup script.
#
# This is also what the code already assumes. app-store/paths.ts derives
# OFFICER_ROOT as dirname(DATA_PATH) and DOCKERS_DIR as OFFICER_ROOT/dockers, so
# setting DATA_PATH to <root>/data is the whole of what makes the layout correct.
[[ -n "${OFFICER_SETUP_LAYOUT_LOADED:-}" ]] && return 0
OFFICER_SETUP_LAYOUT_LOADED=1
layout_data_dir() { echo "${OFFICER_ROOT}/data"; }
layout_dockers_dir() { echo "${OFFICER_ROOT}/dockers"; }
layout_items_dir() { echo "${OFFICER_ROOT}/capabilities"; }
layout_dirs() {
echo "$OFFICER_ROOT"
echo "$(layout_data_dir)"
echo "$(layout_dockers_dir)"
echo "$(layout_items_dir)"
}
# Created owned by the account, because everything that writes into them runs as
# the account: the platform under pm2, the app store's compose files, the item
# store the agent authors into.
create_layout() {
local dir
while read -r dir; do
[[ -d "$dir" ]] || install -d -m 0755 -o "$USERNAME" -g "$(user_group)" "$dir"
done < <(layout_dirs)
return 0
}
# A directory that exists but belongs to somebody else is the failure this
# reports: it happens when an earlier run, or a hand-made directory, was created
# as root, and everything written into it afterwards fails in a way that reads as
# a permissions bug in the platform.
layout_wrong_owner() {
local dir
while read -r dir; do
[[ -d "$dir" ]] || continue
[[ "$(stat -c %U "$dir")" == "$USERNAME" ]] || echo "$dir"
done < <(layout_dirs)
}
# -----------------------------------------------------------------------------
# The capabilities Officer ships
# -----------------------------------------------------------------------------
#
# `capabilities/` is the file-based item store. The platform creates the six item
# directories at boot and writes `extensions/tool-loader` from its own source, so
# those are generated and not tracked anywhere. What IS tracked is the small set
# the platform ships — today one affordance, telling an agent that markdown
# images render and that the user's DEVICE fetches the URL.
#
# Without this a fresh install has no affordances at all, and every agent on it
# goes back to guessing what the client on the other end will do.
#
# Cloned rather than copied out of the platform repository, so it can be updated
# on its own — by `git pull` here, or by "Update all" on the marketplace screen.
CAPABILITIES_REPO="https://gitea.officer.dev/officerdev/capabilities.git"
# Three states, because a directory that already exists is the normal one: the
# layout step created it a moment ago, and a re-run finds a checkout.
capabilities_sync() {
local dir; dir="$(layout_items_dir)"
if [[ -d "$dir/.git" ]]; then
(cd "$dir" && git pull --ff-only --quiet origin main 2>/dev/null) || return 1
return 0
fi
# An empty directory can be cloned into directly. A non-empty one cannot, and
# it is not empty precisely because the platform has already run and written
# its generated directories — so fetch into place instead of refusing, and let
# the owner's own untracked items sit alongside untouched.
if [[ -z "$(ls -A "$dir" 2>/dev/null)" ]]; then
git clone --quiet "$CAPABILITIES_REPO" "$dir" || return 1
else
(cd "$dir" \
&& git init --quiet \
&& git remote add origin "$CAPABILITIES_REPO" 2>/dev/null \
&& git fetch --quiet origin main \
&& git checkout --quiet -B main FETCH_HEAD) || return 1
fi
}