Merge #c1156d20: Terminal developer environment, resumable TUI, and Arc…
Terminal developer environment, resumable TUI, and Archipelago skills nostr:nevent1qqsvz9tdypdv9cmtjmsz6azv8yujk0xezwv0j4wpa57qnk60nfyj28gpz3mhxue69uhhyetvv9ujumn8d96zuer9wcffu24e PR-Author: Personal nostr:npub1w3sqdkrhn0gyuvsex32effzgnfpyde6qrrc4u467flg5e9txh4wsfn5vjg PR description: Adds the authenticated resumable terminal session backend, xterm-based terminal UI with bounded scrolling and app-style controls, tmux key translation, Codex/node bootstrap, preloaded Archipelago skills, developer app starter, and UAT runbook. Validation: npm run build:production; cargo check -p archipelago; scripts/tests/archy-developer-setup-test.sh. Framework UAT health and unauthenticated terminal endpoint checks pass.
This commit is contained in:
Executable
+96
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
# Idempotent Archipelago developer environment bootstrap.
|
||||
# Check by default; package installation requires --apply.
|
||||
set -euo pipefail
|
||||
|
||||
APPLY=0
|
||||
# Codex is part of the standard developer environment. Keep --codex as a
|
||||
# backwards-compatible no-op for existing setup commands.
|
||||
CODEX=1
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--apply) APPLY=1 ;;
|
||||
--codex) CODEX=1 ;;
|
||||
--help|-h) printf 'Usage: archy-developer-setup [--check] [--apply] [--codex]\n\nCodex is installed by default with --apply; authenticate with: codex login\n'; exit 0 ;;
|
||||
--check) ;;
|
||||
*) printf 'archy-developer-setup: unknown option: %s\n' "$arg" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
tools=(git tmux jq fzf rg fd nvim podman mise direnv)
|
||||
missing=()
|
||||
for tool in "${tools[@]}"; do
|
||||
if command -v "$tool" >/dev/null 2>&1; then
|
||||
printf 'ok\t%s\t%s\n' "$tool" "$(command -v "$tool")"
|
||||
else
|
||||
missing+=("$tool")
|
||||
printf 'missing\t%s\n' "$tool"
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v codex >/dev/null 2>&1; then
|
||||
printf 'ok\tcodex\t%s\n' "$(command -v codex)"
|
||||
elif (( CODEX )); then
|
||||
missing+=(codex)
|
||||
printf 'missing\tcodex\n'
|
||||
fi
|
||||
|
||||
user_home=$(getent passwd "$(id -u)" | cut -d: -f6)
|
||||
codex_root=${CODEX_HOME:-$user_home/.codex}
|
||||
skill_source_dir=${ARCHY_SKILLS_DIR:-$PWD/.agents/skills}
|
||||
skills=(archipelago archipelago-app archipelago-design)
|
||||
missing_skills=()
|
||||
for skill in "${skills[@]}"; do
|
||||
source_skill="$skill_source_dir/$skill/SKILL.md"
|
||||
target_skill="$codex_root/skills/$skill/SKILL.md"
|
||||
if [ -f "$target_skill" ]; then
|
||||
printf 'ok\tskill:%s\t%s\n' "$skill" "$target_skill"
|
||||
elif [ -f "$source_skill" ]; then
|
||||
missing_skills+=("skill:$skill")
|
||||
printf 'missing\tskill:%s\t%s\n' "$skill" "$target_skill"
|
||||
else
|
||||
printf 'missing\tskill:%s\tsource unavailable (%s)\n' "$skill" "$source_skill"
|
||||
fi
|
||||
done
|
||||
|
||||
if (( ! APPLY )); then
|
||||
if [ "${#missing[@]}" -gt 0 ] || [ "${#missing_skills[@]}" -gt 0 ]; then
|
||||
printf '\nMissing: %s %s\n' "${missing[*]}" "${missing_skills[*]}"
|
||||
printf 'Re-run with --apply to install missing packages, including Codex.\n'
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v apt-get >/dev/null 2>&1; then
|
||||
printf 'No apt-get found; install the missing tools using the node distribution package manager.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
packages=(git tmux jq fzf ripgrep fd-find neovim podman direnv nodejs npm)
|
||||
if [ "${#missing[@]}" -gt 0 ]; then
|
||||
sudo -n apt-get update
|
||||
sudo -n apt-get install -y "${packages[@]}"
|
||||
fi
|
||||
|
||||
if (( CODEX )) && ! command -v codex >/dev/null 2>&1; then
|
||||
command -v npm >/dev/null 2>&1 || { printf 'npm is required for Codex installation.\n' >&2; exit 1; }
|
||||
sudo -n npm install --global @openai/codex
|
||||
fi
|
||||
|
||||
if [ "${#missing_skills[@]}" -gt 0 ]; then
|
||||
mkdir -p "$codex_root/skills"
|
||||
for skill in "${skills[@]}"; do
|
||||
source_dir="$skill_source_dir/$skill"
|
||||
target_dir="$codex_root/skills/$skill"
|
||||
[ -f "$source_dir/SKILL.md" ] || continue
|
||||
mkdir -p "$target_dir"
|
||||
cp -a "$source_dir/." "$target_dir/"
|
||||
printf 'installed\tskill:%s\t%s\n' "$skill" "$target_dir"
|
||||
done
|
||||
fi
|
||||
|
||||
if command -v mise >/dev/null 2>&1; then
|
||||
mise trust --yes "$PWD" >/dev/null 2>&1 || true
|
||||
fi
|
||||
printf 'developer setup complete; Archipelago skills preloaded for Codex; authenticate interactively with: codex login\n'
|
||||
Executable
+118
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env bash
|
||||
# Persistent Archipelago developer sessions. Terminal attachments are disposable;
|
||||
# the tmux session owns the process and durable JSON owns its discoverable state.
|
||||
set -euo pipefail
|
||||
|
||||
STATE_DIR="${ARCHY_SESSION_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/archipelago/sessions}"
|
||||
mkdir -p "$STATE_DIR"
|
||||
die() { echo "archy-session: $*" >&2; exit 2; }
|
||||
need_tmux() { command -v tmux >/dev/null 2>&1 || die "tmux is required"; }
|
||||
need_python() { command -v python3 >/dev/null 2>&1 || die "python3 is required"; }
|
||||
valid_id() { [[ "$1" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$ ]]; }
|
||||
session_file() { printf '%s/%s.json' "$STATE_DIR" "$1"; }
|
||||
|
||||
write_record() {
|
||||
local id=$1 name=$2 workspace=$3 state=$4 tmux_name=$5
|
||||
need_python
|
||||
python3 - "$STATE_DIR" "$id" "$name" "$workspace" "$state" "$tmux_name" <<'PY'
|
||||
import json, os, sys, tempfile, time
|
||||
state_dir, ident, name, workspace, state, tmux_name = sys.argv[1:]
|
||||
record = {"schema": 1, "id": ident, "name": name, "workspace": workspace,
|
||||
"state": state, "tmux": tmux_name, "updated_at": int(time.time())}
|
||||
fd, tmp = tempfile.mkstemp(prefix=f".{ident}.", suffix=".tmp", dir=state_dir)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
||||
json.dump(record, handle, sort_keys=True); handle.write("\n"); handle.flush(); os.fsync(handle.fileno())
|
||||
os.replace(tmp, os.path.join(state_dir, ident + ".json"))
|
||||
finally:
|
||||
try: os.unlink(tmp)
|
||||
except FileNotFoundError: pass
|
||||
PY
|
||||
}
|
||||
|
||||
new_id() { need_python; python3 - <<'PY'
|
||||
import secrets
|
||||
print("s-" + secrets.token_hex(8))
|
||||
PY
|
||||
}
|
||||
|
||||
read_json() {
|
||||
python3 - "$1" "$2" <<'PY'
|
||||
import json, sys
|
||||
print(json.load(open(sys.argv[1], encoding="utf-8"))[sys.argv[2]])
|
||||
PY
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
need_tmux; need_python
|
||||
python3 - "$STATE_DIR" <<'PY'
|
||||
import json, os, subprocess, sys
|
||||
state_dir = sys.argv[1]
|
||||
for filename in sorted(os.listdir(state_dir)):
|
||||
if not filename.endswith(".json"): continue
|
||||
try: record = json.load(open(os.path.join(state_dir, filename), encoding="utf-8"))
|
||||
except (OSError, ValueError, KeyError): continue
|
||||
live = subprocess.run(["tmux", "has-session", "-t", record["tmux"]], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0
|
||||
effective = "detached" if live else ("interrupted" if record.get("state") == "running" else record.get("state", "ended"))
|
||||
print(f"{record['id']}\t{record['name']}\t{effective}\t{record['workspace']}")
|
||||
PY
|
||||
}
|
||||
|
||||
cmd_start() {
|
||||
need_tmux
|
||||
local name=${1:-Work} workspace=${2:-$HOME/Work}
|
||||
[[ -d "$workspace" ]] || die "workspace does not exist: $workspace"
|
||||
local id tmux_name; id=$(new_id); tmux_name="archy-$id"
|
||||
tmux new-session -d -s "$tmux_name" -c "$workspace"
|
||||
write_record "$id" "$name" "$workspace" running "$tmux_name"
|
||||
printf '%s\n' "$id"
|
||||
}
|
||||
|
||||
record_for() {
|
||||
local id=$1 file; valid_id "$id" || die "invalid session id"; file=$(session_file "$id")
|
||||
[[ -f "$file" ]] || die "session not found: $id"; printf '%s\n' "$file"
|
||||
}
|
||||
|
||||
cmd_attach() {
|
||||
need_tmux; local file=$1 tmux_name; tmux_name=$(read_json "$file" tmux)
|
||||
tmux has-session -t "$tmux_name" 2>/dev/null || die "session is not running; inspect with list"
|
||||
exec tmux attach-session -t "$tmux_name"
|
||||
}
|
||||
|
||||
cmd_rename() {
|
||||
local id=$1 name=$2 file=$3; local workspace tmux_name state=ended
|
||||
workspace=$(read_json "$file" workspace); tmux_name=$(read_json "$file" tmux); need_tmux
|
||||
tmux has-session -t "$tmux_name" 2>/dev/null && state=detached
|
||||
write_record "$id" "$name" "$workspace" "$state" "$tmux_name"
|
||||
}
|
||||
|
||||
cmd_end() {
|
||||
local id=$1 file=$2; local tmux_name name workspace
|
||||
tmux_name=$(read_json "$file" tmux); name=$(read_json "$file" name); workspace=$(read_json "$file" workspace); need_tmux
|
||||
tmux kill-session -t "$tmux_name" 2>/dev/null || true
|
||||
write_record "$id" "$name" "$workspace" ended "$tmux_name"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
Usage:
|
||||
archy-session list
|
||||
archy-session start [name] [workspace]
|
||||
archy-session attach <id>
|
||||
archy-session rename <id> <name>
|
||||
archy-session end <id>
|
||||
|
||||
Closing an attached terminal detaches it. end is the explicit destructive
|
||||
operation and never removes the workspace or its files.
|
||||
EOF
|
||||
}
|
||||
|
||||
command=${1:-}
|
||||
case "$command" in
|
||||
list) cmd_list ;;
|
||||
start) shift; cmd_start "$@" ;;
|
||||
attach) shift; [[ $# == 1 ]] || die "attach requires an id"; file=$(record_for "$1"); cmd_attach "$file" ;;
|
||||
rename) shift; [[ $# == 2 ]] || die "rename requires an id and name"; file=$(record_for "$1"); cmd_rename "$1" "$2" "$file" ;;
|
||||
end) shift; [[ $# == 1 ]] || die "end requires an id"; file=$(record_for "$1"); cmd_end "$1" "$file" ;;
|
||||
*) usage; exit 2 ;;
|
||||
esac
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
|
||||
output=$(PATH=/usr/bin:/bin "$ROOT/scripts/archy-developer-setup" --check 2>&1 || true)
|
||||
grep -qE '^(ok|missing)[[:space:]]' <<<"$output"
|
||||
! grep -q 'sudo -n apt-get' <<<"$output"
|
||||
printf 'archy-developer-setup check is non-mutating\n'
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
|
||||
export ARCHY_SESSION_STATE_DIR="$TMP/state"; mkdir -p "$TMP/work"
|
||||
if ! command -v tmux >/dev/null 2>&1; then echo "SKIP: tmux is unavailable"; exit 0; fi
|
||||
id=$("$ROOT/scripts/archy-session" start demo "$TMP/work")
|
||||
test -f "$TMP/state/$id.json"
|
||||
grep -q "$id" <("$ROOT/scripts/archy-session" list)
|
||||
"$ROOT/scripts/archy-session" rename "$id" renamed
|
||||
grep -q $'renamed\tdetached' <("$ROOT/scripts/archy-session" list)
|
||||
"$ROOT/scripts/archy-session" end "$id"
|
||||
grep -q $'renamed\tended' <("$ROOT/scripts/archy-session" list)
|
||||
test -d "$TMP/work"; echo "archy-session lifecycle passed"
|
||||
Reference in New Issue
Block a user