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
28 changes: 28 additions & 0 deletions .cursor/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
FROM python:3.13-slim-trixie

ENV DEBIAN_FRONTEND=noninteractive
ENV CRYPTOGRAPHY_DONT_BUILD_RUST=1

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
git \
sudo \
curl \
ca-certificates \
build-essential \
gcc \
binutils \
libffi-dev \
libssl-dev \
libxml2-dev \
libxslt1-dev \
libjpeg62-turbo-dev \
libopenblas-dev \
zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*

RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /workspace
41 changes: 41 additions & 0 deletions .cursor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# OctoBot Cursor Cloud environment

CI-parity install and validation for [Cursor Cloud Agents](https://cursor.com/docs/cloud-agent/setup).

## Profiles (`OCTOBOT_CLOUD_PROFILE`)

| Profile | Layers |
|---------|--------|
| `package-only` | L1–L3 (no tentacles install) |
| `ci-tentacles` | L1–L5 (**default** in `environment.json`) |
| `ui-node-web` | L1–L6 (includes `node_web_interface` build) |

Install: `bash .cursor/cloud-install.sh` (profile via env var).

## Agent / shell usage

```bash
source .cursor/env.sh
# Python, OctoBot CLI, pytest
```

After editing `packages/tentacles/`: `bash .cursor/reinstall-tentacles.sh` (do not edit repo-root `tentacles/`).

## Validate environment (on demand)

```bash
source .cursor/env.sh
bash .cursor/cloud-validate.sh --profile ci-tentacles --json
```

Not run from install; use after Build changes or when debugging a broken env.

## Debugging failures

| Surface | What to search |
|---------|----------------|
| Cursor Cloud Build log | `OCTOBOT_CLOUD_FAILED` or `==> [L` step markers |
| GitHub `cloud-env-validate` | Same markers; `cloud-validate.json` artifact on failure |
| Success install | `OCTOBOT_CLOUD_INSTALL_OK` |

If a dashboard environment snapshot shadows this repo config, remove the saved snapshot so committed `environment.json` wins.
55 changes: 55 additions & 0 deletions .cursor/cloud-install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/cloud-shell-lib.sh"
cloud_lib_init "cloud-install.sh"

echo "OCTOBOT_CLOUD_PROFILE=${CLOUD_PROFILE}"
log_step "INIT" "repo=${REPO_ROOT}"

if cloud_profile_includes_layer "L1"; then
log_step "L1" "Toolchain (Pants, optional Rust)"
if ! command -v pants >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -fsSL https://static.pantsbuild.org/setup/get-pants.sh | bash
fi
export PATH="${HOME}/.local/bin:${PATH}"
if compgen -G "packages/*/crates/*/Cargo.toml" >/dev/null 2>&1; then
if ! command -v cargo >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
# shellcheck disable=SC1091
source "${HOME}/.cargo/env"
fi
fi
fi

if cloud_profile_includes_layer "L2"; then
log_step "L2" "Pants package OctoBot wheel"
export PATH="${HOME}/.local/bin:${PATH}"
pants package :OctoBot $(pants list --filter-target-type=package_shell_command ::)
fi

if cloud_profile_includes_layer "L3"; then
log_step "L3" "Python venv and OctoBot wheel"
python3 -m venv "${REPO_ROOT}/.cursor/venv"
cloud_write_env_files
# shellcheck disable=SC1091
source "${REPO_ROOT}/.cursor/env.sh"
pip install --upgrade pip setuptools wheel
pip install -r dev_requirements.txt
pip install dist/*.whl
if compgen -G "packages/*/crates/*/Cargo.toml" >/dev/null 2>&1; then
pip install maturin
fi
fi

if cloud_profile_includes_layer "L4"; then
cloud_npm_client_build
fi

if cloud_profile_includes_layer "L5"; then
cloud_tentacles_pack_and_install
fi

if [[ "${CLOUD_PROFILE}" == "ui-node-web" ]]; then
cloud_node_web_interface_build
fi

echo "OCTOBOT_CLOUD_INSTALL_OK profile=${CLOUD_PROFILE}"
109 changes: 109 additions & 0 deletions .cursor/cloud-shell-lib.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Shared helpers for OctoBot Cursor Cloud shell scripts.
# shellcheck shell=bash

cloud_lib_init() {
local script_name="$1"
CLOUD_SCRIPT_NAME="$script_name"
set -euo pipefail
CLOUD_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${CLOUD_LIB_DIR}/.." && pwd)"
cd "${REPO_ROOT}"
CLOUD_PROFILE="${OCTOBOT_CLOUD_PROFILE:-ci-tentacles}"
CLOUD_LAST_LOG_TAIL=""
trap 'cloud_err_trap' ERR
}

log_step() {
local step_id="$1"
local message="$2"
echo "==> [${step_id}] ${message}"
}

die() {
local step_id="$1"
local message="$2"
echo "OCTOBOT_CLOUD_FAILED script=${CLOUD_SCRIPT_NAME} step=${step_id} profile=${CLOUD_PROFILE} message=${message}" >&2
if [[ -n "${CLOUD_LAST_LOG_TAIL}" && -f "${CLOUD_LAST_LOG_TAIL}" ]]; then
echo "--- last command output (tail) ---" >&2
tail -n 30 "${CLOUD_LAST_LOG_TAIL}" >&2 || true
fi
exit 1
}

cloud_err_trap() {
local step_id="${CLOUD_TRAP_STEP:-ERR}"
die "${step_id}" "command failed: ${BASH_COMMAND}"
}

cloud_profile_includes_layer() {
local layer="$1"
case "${CLOUD_PROFILE}" in
package-only)
[[ "${layer}" == "L1" || "${layer}" == "L2" || "${layer}" == "L3" ]] && return 0
return 1
;;
ci-tentacles)
[[ "${layer}" == "L6" ]] && return 1
return 0
;;
ui-node-web)
return 0
;;
*)
die "INIT" "unknown OCTOBOT_CLOUD_PROFILE=${CLOUD_PROFILE}"
;;
esac
}

cloud_write_env_files() {
log_step "ENV_FILES" "Writing .cursor/env.sh and .cursor/pythonpath.sh"
cat >"${REPO_ROOT}/.cursor/env.sh" <<EOF
# Generated by cloud-install.sh — source before Python, OctoBot, pytest
export OCTOBOT_REPO_ROOT="${REPO_ROOT}"
export OCTOBOT_CLOUD_VENV="${REPO_ROOT}/.cursor/venv"
export DISABLE_SENTRY=True
# Prefer the repo-pinned Node (.nvmrc) over any platform-provided node earlier in PATH.
# Prepended before the venv so the venv still wins for python/OctoBot below.
_octobot_nvmrc_major="\$(tr -dc '0-9.' < "${REPO_ROOT}/.nvmrc" 2>/dev/null | cut -d. -f1)"
for _octobot_node in /usr/bin/node /usr/local/bin/node; do
if [ -x "\$_octobot_node" ]; then
_octobot_node_major="\$("\$_octobot_node" --version 2>/dev/null | tr -dc '0-9.' | cut -d. -f1)"
if [ -n "\$_octobot_nvmrc_major" ] && [ "\$_octobot_node_major" = "\$_octobot_nvmrc_major" ]; then
export PATH="\$(dirname "\$_octobot_node"):\${PATH}"
break
fi
fi
done
unset _octobot_nvmrc_major _octobot_node _octobot_node_major
# venv bin must remain first so python/OctoBot resolve to the OctoBot venv.
export PATH="\${OCTOBOT_CLOUD_VENV}/bin:\${PATH}"
EOF
cat >"${REPO_ROOT}/.cursor/pythonpath.sh" <<'EOF'
# For packages/node, packages/flow, packages/copy tests from repo root (matches CI)
export PYTHONPATH=".:${PYTHONPATH:-}"
EOF
}

cloud_tentacles_pack_and_install() {
log_step "TENTACLES_PACK" "OctoBot tentacles pack from packages/tentacles"
mkdir -p "${REPO_ROOT}/output"
export ALLOW_UNSIGNED_TENTACLES=true
# shellcheck disable=SC1091
source "${REPO_ROOT}/.cursor/env.sh"
# -p is package filename only; default output_dir is output/ (see main.yml tentacles install)
OctoBot tentacles -d packages/tentacles -p any_platform.zip
log_step "TENTACLES_INSTALL" "OctoBot tentacles install to repo root"
OctoBot tentacles --install --location output/any_platform.zip --all
}

cloud_npm_client_build() {
log_step "L4" "npm ci and build octobot_client_ts"
npm ci
npm run build -w packages/client/octobot_client_ts
}

cloud_node_web_interface_build() {
local ui_dir="${REPO_ROOT}/packages/tentacles/Services/Interfaces/node_web_interface"
log_step "L6" "npm ci and build node_web_interface"
(cd "${ui_dir}" && npm ci && npm run build)
}
157 changes: 157 additions & 0 deletions .cursor/cloud-validate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#!/usr/bin/env bash
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/cloud-shell-lib.sh"
cloud_lib_init "cloud-validate.sh"

EMIT_JSON=0
while [[ $# -gt 0 ]]; do
case "$1" in
--profile) CLOUD_PROFILE="$2"; shift 2 ;;
--json) EMIT_JSON=1; shift ;;
*) die "ARGS" "unknown argument $1" ;;
esac
done

VALIDATE_STEPS=()
VALIDATE_OK=1
VALIDATE_FAILED_STEP=""
VALIDATE_MESSAGE=""

validate_record() {
local name="$1"
local ok="$2"
local detail="$3"
VALIDATE_STEPS+=("${name}|${ok}|${detail}")
if [[ "${ok}" != "1" ]]; then
VALIDATE_OK=0
VALIDATE_FAILED_STEP="${name}"
VALIDATE_MESSAGE="${detail}"
return 1
fi
return 0
}

validate_emit_json() {
export VALIDATE_OK="${VALIDATE_OK}"
export VALIDATE_FAILED_STEP="${VALIDATE_FAILED_STEP}"
export VALIDATE_MESSAGE="${VALIDATE_MESSAGE}"
export CLOUD_PROFILE="${CLOUD_PROFILE}"
export VALIDATE_STEP_LINES
VALIDATE_STEP_LINES="$(printf '%s\n' "${VALIDATE_STEPS[@]}")"
python3 - <<'PY'
import json
import os

steps = []
for entry in os.environ.get("VALIDATE_STEP_LINES", "").split("\n"):
if not entry.strip():
continue
name, ok, detail = entry.split("|", 2)
steps.append({"name": name, "ok": ok == "1", "detail": detail})

failed = os.environ.get("VALIDATE_FAILED_STEP") or None
message = os.environ.get("VALIDATE_MESSAGE") or None
if failed == "":
failed = None
if message == "":
message = None

payload = {
"ok": os.environ.get("VALIDATE_OK") == "1",
"profile": os.environ.get("CLOUD_PROFILE"),
"failed_step": failed,
"message": message,
"steps": steps,
}
print(json.dumps(payload, indent=2))
PY
}

validate_finish() {
if [[ "${EMIT_JSON}" == "1" ]]; then
validate_emit_json | tee "${REPO_ROOT}/cloud-validate.json"
fi
if [[ "${VALIDATE_OK}" != "1" ]]; then
die "${VALIDATE_FAILED_STEP}" "${VALIDATE_MESSAGE}"
fi
exit 0
}

[[ -f "${REPO_ROOT}/.cursor/env.sh" ]] || die "VALIDATE_ENV" "missing .cursor/env.sh — run cloud-install.sh"
# shellcheck disable=SC1091
source "${REPO_ROOT}/.cursor/env.sh"

log_step "VALIDATE_PYTHON_VERSION" "python 3.13.x"
pyver="$(python --version 2>&1)" || die "VALIDATE_PYTHON_VERSION" "python not runnable"
if [[ "${pyver}" != *"3.13"* ]]; then
validate_record "VALIDATE_PYTHON_VERSION" 0 "${pyver}" || validate_finish
fi
validate_record "VALIDATE_PYTHON_VERSION" 1 "ok" || validate_finish

log_step "VALIDATE_OCTOBOT_CLI" "OctoBot on PATH"
if ! OctoBot --version >/dev/null; then
validate_record "VALIDATE_OCTOBOT_CLI" 0 "OctoBot CLI failed" || validate_finish
fi
validate_record "VALIDATE_OCTOBOT_CLI" 1 "ok" || validate_finish

log_step "VALIDATE_IMPORT_OCTOBOT" "import octobot"
if ! python -c "import octobot"; then
validate_record "VALIDATE_IMPORT_OCTOBOT" 0 "import failed" || validate_finish
fi
validate_record "VALIDATE_IMPORT_OCTOBOT" 1 "ok" || validate_finish

needs_tentacles=false
[[ "${CLOUD_PROFILE}" != "package-only" ]] && needs_tentacles=true

if [[ "${needs_tentacles}" == "true" ]]; then
log_step "VALIDATE_TENTACLES_DIR" "repo-root tentacles/"
if [[ ! -d tentacles ]] || [[ -z "$(ls -A tentacles 2>/dev/null)" ]]; then
validate_record "VALIDATE_TENTACLES_DIR" 0 "missing or empty" || validate_finish
fi
validate_record "VALIDATE_TENTACLES_DIR" 1 "ok" || validate_finish

log_step "VALIDATE_IMPORT_TENTACLES" "import tentacles"
if ! python -c "import tentacles"; then
validate_record "VALIDATE_IMPORT_TENTACLES" 0 "import failed" || validate_finish
fi
validate_record "VALIDATE_IMPORT_TENTACLES" 1 "ok" || validate_finish

client_dist="packages/client/octobot_client_ts/dist/index.js"
log_step "VALIDATE_CLIENT_BUILD" "${client_dist}"
if [[ ! -f "${client_dist}" ]]; then
validate_record "VALIDATE_CLIENT_BUILD" 0 "missing" || validate_finish
fi
validate_record "VALIDATE_CLIENT_BUILD" 1 "ok" || validate_finish
fi

log_step "VALIDATE_SENTINEL_OCTOBOT" "pytest collect-only"
if ! pytest --collect-only tests/unit_tests/test_octobot_version.py >/dev/null; then
validate_record "VALIDATE_SENTINEL_OCTOBOT" 0 "collect failed" || validate_finish
fi
validate_record "VALIDATE_SENTINEL_OCTOBOT" 1 "ok" || validate_finish

log_step "VALIDATE_SENTINEL_COMMONS" "pytest collect-only"
if ! (cd packages/commons && pytest --collect-only tests/test_copy_util.py >/dev/null); then
validate_record "VALIDATE_SENTINEL_COMMONS" 0 "collect failed" || validate_finish
fi
validate_record "VALIDATE_SENTINEL_COMMONS" 1 "ok" || validate_finish

if [[ "${needs_tentacles}" == "true" ]]; then
log_step "VALIDATE_SENTINEL_FLOW_COLLECT" "pytest collect-only with PYTHONPATH"
# shellcheck disable=SC1091
source "${REPO_ROOT}/.cursor/pythonpath.sh"
if ! pytest --collect-only packages/flow/tests/protocol/test_wire_compat.py >/dev/null; then
validate_record "VALIDATE_SENTINEL_FLOW_COLLECT" 0 "collect failed" || validate_finish
fi
validate_record "VALIDATE_SENTINEL_FLOW_COLLECT" 1 "ok" || validate_finish
fi

if [[ "${CLOUD_PROFILE}" == "ui-node-web" ]]; then
ui_dist="packages/tentacles/Services/Interfaces/node_web_interface/dist"
log_step "VALIDATE_UI_BUILD" "${ui_dist}"
if [[ ! -d "${ui_dist}" ]]; then
validate_record "VALIDATE_UI_BUILD" 0 "missing" || validate_finish
fi
validate_record "VALIDATE_UI_BUILD" 1 "ok" || validate_finish
fi

validate_finish
Loading
Loading