feat(dev-hub): custodian dev up and files-first local hub (CUST-WP-0054-T07)
Add dev_hub_up.sh, mcp_hub_profile.sh, Makefile targets, and custodian dev up subcommand for offline-capable local hub bootstrap.
This commit is contained in:
parent
931ebfb652
commit
2d6b5f0150
4 changed files with 250 additions and 1 deletions
19
Makefile
19
Makefile
|
|
@ -1,4 +1,4 @@
|
||||||
.PHONY: install install-cli dashboard-install dashboard-check db db-tools migrate seed api dashboard check test test-python clean register-project register-codex-project register-mcp bootstrap-env validate-adr add-domain rename-domain add-repo list-repos register-path register-from-classification register-from-classification-all cleanup-stale tunnels-up tunnels-status tunnels-check bridges install-hooks install-hooks-all gitea-inventory token-reconcile railiance-state-hub-render railiance-state-hub-client-dry-run railiance-state-hub-server-dry-run
|
.PHONY: install install-cli dashboard-install dashboard-check db db-tools migrate seed api dashboard check test test-python clean register-project register-codex-project register-mcp bootstrap-env dev-hub edge-relay mcp-profile validate-adr add-domain rename-domain add-repo list-repos register-path register-from-classification register-from-classification-all cleanup-stale tunnels-up tunnels-status tunnels-check bridges install-hooks install-hooks-all gitea-inventory token-reconcile railiance-state-hub-render railiance-state-hub-client-dry-run railiance-state-hub-server-dry-run
|
||||||
|
|
||||||
COMPOSE = docker compose -f infra/docker-compose.yml --env-file .env
|
COMPOSE = docker compose -f infra/docker-compose.yml --env-file .env
|
||||||
PYTHON ?= python3
|
PYTHON ?= python3
|
||||||
|
|
@ -184,6 +184,23 @@ register-mcp:
|
||||||
bootstrap-env:
|
bootstrap-env:
|
||||||
scripts/bootstrap-env.sh $(ARGS)
|
scripts/bootstrap-env.sh $(ARGS)
|
||||||
|
|
||||||
|
## Files-first local dev hub (CUST-WP-0054-T07): postgres + migrate + seed + API + repo registration.
|
||||||
|
## Optional: WITH_EDGE=1 WITH_MCP=1 make dev-hub
|
||||||
|
## Fleet tunnel when reachable: make dev-hub PROFILE=fleet
|
||||||
|
dev-hub:
|
||||||
|
bash scripts/dev_hub_up.sh $(if $(PROFILE),$(PROFILE),dev)
|
||||||
|
|
||||||
|
## Local edge relay for offline write buffering (STATE-WP-0068).
|
||||||
|
edge-relay:
|
||||||
|
@fuser -k 18080/tcp 2>/dev/null && echo "Stopped running edge relay" || true
|
||||||
|
STATEHUB_UPSTREAM_URL=$${STATEHUB_UPSTREAM_URL:-http://127.0.0.1:8000} \
|
||||||
|
$(UV) run uvicorn api.edge.relay:app --host 127.0.0.1 --port 18080
|
||||||
|
|
||||||
|
## Switch MCP/API profile: make mcp-profile PROFILE=dev|fleet|status
|
||||||
|
mcp-profile:
|
||||||
|
@test -n "$(PROFILE)" || (echo "ERROR: PROFILE is required (dev|fleet|status)."; exit 1)
|
||||||
|
bash scripts/mcp_hub_profile.sh "$(PROFILE)"
|
||||||
|
|
||||||
## Add a second repo to an existing domain: make add-repo DOMAIN=railiance REPO_PATH=/home/worsch/railiance-infra
|
## Add a second repo to an existing domain: make add-repo DOMAIN=railiance REPO_PATH=/home/worsch/railiance-infra
|
||||||
add-repo:
|
add-repo:
|
||||||
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required."; exit 1)
|
@test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required."; exit 1)
|
||||||
|
|
|
||||||
|
|
@ -565,6 +565,27 @@ def cmd_outbox_cancel(args: argparse.Namespace) -> None:
|
||||||
store.cancel(args.envelope_id)
|
store.cancel(args.envelope_id)
|
||||||
print(f"Cancelled {args.envelope_id}")
|
print(f"Cancelled {args.envelope_id}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_dev_up(args: argparse.Namespace) -> None:
|
||||||
|
"""Start files-first local dev hub (compose path via dev_hub_up.sh)."""
|
||||||
|
script = STATE_HUB_DIR / "scripts" / "dev_hub_up.sh"
|
||||||
|
if not script.exists():
|
||||||
|
print(f"ERROR: dev hub script not found at {script}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
if args.with_edge:
|
||||||
|
env["WITH_EDGE"] = "1"
|
||||||
|
if args.with_mcp:
|
||||||
|
env["WITH_MCP"] = "1"
|
||||||
|
if args.repo_root:
|
||||||
|
env["REPO_ROOT"] = str(Path(args.repo_root).expanduser().resolve())
|
||||||
|
|
||||||
|
cmd = ["bash", str(script), args.profile]
|
||||||
|
result = subprocess.run(cmd, env=env, cwd=STATE_HUB_DIR)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
|
|
||||||
# ── Entry point ────────────────────────────────────────────────────────────────
|
# ── Entry point ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
|
@ -709,6 +730,25 @@ def main() -> None:
|
||||||
# status
|
# status
|
||||||
sub.add_parser("status", help="Show State Hub health and summary totals")
|
sub.add_parser("status", help="Show State Hub health and summary totals")
|
||||||
|
|
||||||
|
# dev up — files-first local hub (CUST-WP-0054-T07)
|
||||||
|
dev = sub.add_parser("dev", help="Local dev-hub commands")
|
||||||
|
dev_sub = dev.add_subparsers(dest="dev_command", required=True)
|
||||||
|
dev_up = dev_sub.add_parser("up", help="Start local dev hub from repo files")
|
||||||
|
dev_up.add_argument(
|
||||||
|
"--profile",
|
||||||
|
choices=["dev", "fleet"],
|
||||||
|
default="dev",
|
||||||
|
help="dev=local :8000; fleet=prefer tunnel :18000 when reachable",
|
||||||
|
)
|
||||||
|
dev_up.add_argument("--with-edge", action="store_true", help="Start edge relay on :18080")
|
||||||
|
dev_up.add_argument("--with-mcp", action="store_true", help="Register dev-hub MCP after API is up")
|
||||||
|
dev_up.add_argument(
|
||||||
|
"--repo-root",
|
||||||
|
default=None,
|
||||||
|
help="Parent directory for register-from-classification-all (default: $HOME)",
|
||||||
|
)
|
||||||
|
dev_up.set_defaults(func=cmd_dev_up)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if hasattr(args, "func"):
|
if hasattr(args, "func"):
|
||||||
|
|
|
||||||
104
scripts/dev_hub_up.sh
Executable file
104
scripts/dev_hub_up.sh
Executable file
|
|
@ -0,0 +1,104 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Start a files-first local dev-hub: postgres, migrate, seed, API, optional edge relay.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
STATE_HUB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$STATE_HUB_DIR"
|
||||||
|
|
||||||
|
PROFILE="${1:-dev}"
|
||||||
|
WITH_EDGE="${WITH_EDGE:-0}"
|
||||||
|
WITH_MCP="${WITH_MCP:-0}"
|
||||||
|
REPO_ROOT="${REPO_ROOT:-$HOME}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Usage: scripts/dev_hub_up.sh [dev|fleet]
|
||||||
|
|
||||||
|
dev — local ephemeral hub on :8000 (default)
|
||||||
|
fleet — prefer fleet tunnel API on :18000 when reachable
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
WITH_EDGE=1 Start edge relay on :18080 after API is up
|
||||||
|
WITH_MCP=1 Register dev-hub MCP after API is up
|
||||||
|
REPO_ROOT= Parent directory for register-from-classification-all (default: $HOME)
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
api_healthy() {
|
||||||
|
curl -fsS --max-time 2 "${1%/}/state/health" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
pick_api_base() {
|
||||||
|
if [[ "$PROFILE" == "fleet" ]]; then
|
||||||
|
if api_healthy "http://127.0.0.1:18000"; then
|
||||||
|
echo "http://127.0.0.1:18000"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo "WARN: fleet profile requested but :18000 unreachable; falling back to local :8000" >&2
|
||||||
|
fi
|
||||||
|
echo "http://127.0.0.1:8000"
|
||||||
|
}
|
||||||
|
|
||||||
|
API_BASE="$(pick_api_base)"
|
||||||
|
export API_BASE
|
||||||
|
|
||||||
|
echo "==> Dev hub profile: ${PROFILE}"
|
||||||
|
echo "==> API_BASE: ${API_BASE}"
|
||||||
|
|
||||||
|
if [[ "$API_BASE" == "http://127.0.0.1:8000" ]]; then
|
||||||
|
echo "==> Starting local postgres + migrations"
|
||||||
|
make db
|
||||||
|
make migrate
|
||||||
|
make seed
|
||||||
|
|
||||||
|
if api_healthy "$API_BASE"; then
|
||||||
|
echo "==> API already running on :8000"
|
||||||
|
else
|
||||||
|
echo "==> Starting API on :8000 (background)"
|
||||||
|
fuser -k 8000/tcp 2>/dev/null || true
|
||||||
|
nohup "$(command -v uv || echo uv)" run uvicorn api.main:app \
|
||||||
|
--host 127.0.0.1 --port 8000 \
|
||||||
|
>"${STATE_HUB_DIR}/.dev-hub-api.log" 2>&1 &
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
api_healthy "$API_BASE" && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
api_healthy "$API_BASE" || {
|
||||||
|
echo "ERROR: API failed to start; see .dev-hub-api.log" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "==> API healthy"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Registering local repos from classification files under ${REPO_ROOT}"
|
||||||
|
API_BASE="$API_BASE" make register-from-classification-all || {
|
||||||
|
echo "WARN: register-from-classification-all had errors (continuing)" >&2
|
||||||
|
}
|
||||||
|
else
|
||||||
|
echo "==> Fleet profile: using remote hub; skipping local postgres/migrate/seed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$WITH_EDGE" == "1" && "$API_BASE" == "http://127.0.0.1:8000" ]]; then
|
||||||
|
echo "==> Starting edge relay on :18080"
|
||||||
|
fuser -k 18080/tcp 2>/dev/null || true
|
||||||
|
STATEHUB_UPSTREAM_URL="$API_BASE" nohup "$(command -v uv || echo uv)" run uvicorn api.edge.relay:app \
|
||||||
|
--host 127.0.0.1 --port 18080 \
|
||||||
|
>"${STATE_HUB_DIR}/.dev-hub-edge.log" 2>&1 &
|
||||||
|
echo " Edge relay: http://127.0.0.1:18080 (set API_BASE to this for offline buffering)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$WITH_MCP" == "1" ]]; then
|
||||||
|
make register-mcp API_BASE="$API_BASE" || echo "WARN: MCP registration skipped" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Dev hub ready."
|
||||||
|
echo " API: ${API_BASE}"
|
||||||
|
echo " Profile: ${PROFILE}"
|
||||||
|
echo " Replay: statehub outbox replay --upstream-url ${API_BASE}"
|
||||||
|
echo " Consistency: statehub fix-consistency --repo <slug> --api-base ${API_BASE}"
|
||||||
88
scripts/mcp_hub_profile.sh
Executable file
88
scripts/mcp_hub_profile.sh
Executable file
|
|
@ -0,0 +1,88 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Switch State Hub MCP/API profile between local dev and fleet tunnel.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PROFILE="${1:-}"
|
||||||
|
CLAUDE_JSON="${CLAUDE_JSON:-$HOME/.claude.json}"
|
||||||
|
STATE_HUB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Usage: scripts/mcp_hub_profile.sh <dev|fleet|status>
|
||||||
|
|
||||||
|
dev — local dev-hub API :8000, MCP :8001
|
||||||
|
fleet — fleet tunnel API :18000, MCP :18001
|
||||||
|
status — show detected profile and URLs
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
api_healthy() {
|
||||||
|
curl -fsS --max-time 2 "${1%/}/state/health" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_profile() {
|
||||||
|
if api_healthy "http://127.0.0.1:18000"; then
|
||||||
|
echo fleet
|
||||||
|
elif api_healthy "http://127.0.0.1:8000"; then
|
||||||
|
echo dev
|
||||||
|
else
|
||||||
|
echo unknown
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
profile_urls() {
|
||||||
|
case "$1" in
|
||||||
|
dev)
|
||||||
|
API_BASE="http://127.0.0.1:8000"
|
||||||
|
MCP_URL="http://127.0.0.1:8001/sse"
|
||||||
|
;;
|
||||||
|
fleet)
|
||||||
|
API_BASE="http://127.0.0.1:18000"
|
||||||
|
MCP_URL="http://127.0.0.1:18001/sse"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: unknown profile $1" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -z "$PROFILE" || "$PROFILE" == "-h" || "$PROFILE" == "--help" ]]; then
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$PROFILE" == "status" ]]; then
|
||||||
|
current="$(detect_profile)"
|
||||||
|
echo "detected_profile=${current}"
|
||||||
|
if [[ "$current" != "unknown" ]]; then
|
||||||
|
profile_urls "$current"
|
||||||
|
echo "API_BASE=${API_BASE}"
|
||||||
|
echo "MCP_URL=${MCP_URL}"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
profile_urls "$PROFILE"
|
||||||
|
echo "==> Switching to profile: ${PROFILE}"
|
||||||
|
echo " API_BASE=${API_BASE}"
|
||||||
|
echo " MCP_URL=${MCP_URL}"
|
||||||
|
|
||||||
|
if api_healthy "$API_BASE"; then
|
||||||
|
echo "OK: API reachable at ${API_BASE}/state/health"
|
||||||
|
else
|
||||||
|
echo "WARN: API not reachable at ${API_BASE}/state/health"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v claude >/dev/null 2>&1; then
|
||||||
|
CONFIG="$(python3 -c "import json; print(json.dumps({'type':'sse','url':'${MCP_URL}'}))")"
|
||||||
|
claude mcp add-json -s user dev-hub "$CONFIG" 2>/dev/null || true
|
||||||
|
echo "OK: registered dev-hub MCP → ${MCP_URL}"
|
||||||
|
else
|
||||||
|
echo "WARN: claude CLI not found; set MCP manually to ${MCP_URL}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
profile_file="${STATE_HUB_DIR}/.hub-profile"
|
||||||
|
printf 'API_BASE=%s\nMCP_URL=%s\nPROFILE=%s\n' "$API_BASE" "$MCP_URL" "$PROFILE" >"$profile_file"
|
||||||
|
echo "OK: wrote ${profile_file}"
|
||||||
|
echo "Export: export API_BASE=${API_BASE}"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue