nexo-brain 5.3.27 → 5.3.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -1
- package/bin/nexo-brain.js +24 -7
- package/package.json +1 -1
- package/src/auto_update.py +37 -16
- package/src/cli.py +23 -0
- package/src/desktop_bridge.py +459 -0
- package/src/hook_guardrails.py +44 -0
- package/src/plugin_loader.py +5 -0
- package/src/plugins/update.py +5 -4
- package/src/scripts/nexo-cron-wrapper.sh +78 -22
- package/src/scripts/nexo-update.sh +14 -288
- package/src/server.py +140 -99
- package/src/tree_hygiene.py +56 -0
|
@@ -13,6 +13,7 @@ shift
|
|
|
13
13
|
|
|
14
14
|
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
15
15
|
DB="$NEXO_HOME/data/nexo.db"
|
|
16
|
+
SPOOL_DIR="$NEXO_HOME/operations/cron-spool"
|
|
16
17
|
|
|
17
18
|
# Unlock macOS Keychain so headless Claude Code can read auth tokens.
|
|
18
19
|
# Claude Code stores its API key in the login keychain which auto-locks.
|
|
@@ -21,40 +22,95 @@ if [ -f "$KEYCHAIN_PASS_FILE" ] && [ "$(uname)" = "Darwin" ]; then
|
|
|
21
22
|
security unlock-keychain -p "$(cat "$KEYCHAIN_PASS_FILE")" ~/Library/Keychains/login.keychain-db 2>/dev/null || true
|
|
22
23
|
fi
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
START_EPOCH=$(python3 - <<'PY'
|
|
26
|
+
import time
|
|
27
|
+
print(f"{time.time():.6f}")
|
|
28
|
+
PY
|
|
29
|
+
)
|
|
30
|
+
STARTED_AT=$(python3 - <<'PY'
|
|
31
|
+
from datetime import datetime, timezone
|
|
32
|
+
print(datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S"))
|
|
33
|
+
PY
|
|
34
|
+
)
|
|
31
35
|
|
|
32
36
|
# Run the actual command, capture output
|
|
33
37
|
OUTPUT_FILE=$(mktemp)
|
|
38
|
+
trap 'rm -f "$OUTPUT_FILE"' EXIT
|
|
34
39
|
"$@" > "$OUTPUT_FILE" 2>&1
|
|
35
40
|
EXIT_CODE=$?
|
|
41
|
+
ENDED_AT=$(python3 - <<'PY'
|
|
42
|
+
from datetime import datetime, timezone
|
|
43
|
+
print(datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S"))
|
|
44
|
+
PY
|
|
45
|
+
)
|
|
46
|
+
DURATION_SECS=$(python3 - <<PY
|
|
47
|
+
start = float("$START_EPOCH")
|
|
48
|
+
import time
|
|
49
|
+
print(round(time.time() - start, 1))
|
|
50
|
+
PY
|
|
51
|
+
)
|
|
36
52
|
|
|
37
53
|
# Extract summary (last meaningful line, max 500 chars)
|
|
38
|
-
SUMMARY=$(tail -5 "$OUTPUT_FILE" | grep -v "^$" | tail -1 | head -c 500
|
|
54
|
+
SUMMARY=$(tail -5 "$OUTPUT_FILE" | grep -v "^$" | tail -1 | head -c 500)
|
|
39
55
|
|
|
40
56
|
# Extract error if failed
|
|
41
57
|
ERROR=""
|
|
42
58
|
if [ $EXIT_CODE -ne 0 ]; then
|
|
43
|
-
ERROR=$(grep -i "error\|exception\|fail\|traceback" "$OUTPUT_FILE" | tail -1 | head -c 500
|
|
59
|
+
ERROR=$(grep -i "error\|exception\|fail\|traceback" "$OUTPUT_FILE" | tail -1 | head -c 500)
|
|
44
60
|
fi
|
|
45
61
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
if ! python3 - "$DB" "$CRON_ID" "$STARTED_AT" "$ENDED_AT" "$EXIT_CODE" "$SUMMARY" "$ERROR" "$DURATION_SECS" <<'PY'
|
|
63
|
+
from __future__ import annotations
|
|
64
|
+
|
|
65
|
+
import sqlite3
|
|
66
|
+
import sys
|
|
67
|
+
|
|
68
|
+
db_path, cron_id, started_at, ended_at, exit_code, summary, error, duration_secs = sys.argv[1:]
|
|
69
|
+
conn = sqlite3.connect(db_path)
|
|
70
|
+
try:
|
|
71
|
+
conn.execute(
|
|
72
|
+
"""
|
|
73
|
+
INSERT INTO cron_runs (
|
|
74
|
+
cron_id, started_at, ended_at, exit_code, summary, error, duration_secs
|
|
75
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
76
|
+
""",
|
|
77
|
+
(
|
|
78
|
+
cron_id,
|
|
79
|
+
started_at,
|
|
80
|
+
ended_at,
|
|
81
|
+
int(exit_code),
|
|
82
|
+
summary,
|
|
83
|
+
error,
|
|
84
|
+
float(duration_secs),
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
conn.commit()
|
|
88
|
+
finally:
|
|
89
|
+
conn.close()
|
|
90
|
+
PY
|
|
91
|
+
then
|
|
92
|
+
mkdir -p "$SPOOL_DIR"
|
|
93
|
+
SPOOL_FILE="$SPOOL_DIR/${CRON_ID}-$(date +%Y%m%d-%H%M%S)-$$.json"
|
|
94
|
+
python3 - "$SPOOL_FILE" "$CRON_ID" "$STARTED_AT" "$ENDED_AT" "$EXIT_CODE" "$SUMMARY" "$ERROR" "$DURATION_SECS" <<'PY'
|
|
95
|
+
from __future__ import annotations
|
|
96
|
+
|
|
97
|
+
import json
|
|
98
|
+
import sys
|
|
99
|
+
from pathlib import Path
|
|
100
|
+
|
|
101
|
+
spool_file, cron_id, started_at, ended_at, exit_code, summary, error, duration_secs = sys.argv[1:]
|
|
102
|
+
payload = {
|
|
103
|
+
"cron_id": cron_id,
|
|
104
|
+
"started_at": started_at,
|
|
105
|
+
"ended_at": ended_at,
|
|
106
|
+
"exit_code": int(exit_code),
|
|
107
|
+
"summary": summary,
|
|
108
|
+
"error": error,
|
|
109
|
+
"duration_secs": float(duration_secs),
|
|
110
|
+
}
|
|
111
|
+
Path(spool_file).write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
112
|
+
PY
|
|
113
|
+
echo "[nexo-cron-wrapper] DB write failed; spooled run to $SPOOL_FILE" >&2
|
|
114
|
+
fi
|
|
59
115
|
|
|
60
116
|
exit $EXIT_CODE
|
|
@@ -1,306 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# nexo-update.sh —
|
|
3
|
-
# Same logic as the MCP tool but usable when the server itself needs updating.
|
|
4
|
-
#
|
|
2
|
+
# nexo-update.sh — Thin wrapper around the canonical Python update core.
|
|
5
3
|
# Usage:
|
|
6
4
|
# nexo-update.sh # pull from origin main
|
|
7
5
|
# nexo-update.sh origin beta # pull from origin beta
|
|
8
|
-
# NEXO_HOME=/path nexo-update.sh # custom NEXO_HOME
|
|
9
6
|
|
|
10
7
|
set -euo pipefail
|
|
11
8
|
|
|
12
|
-
# --- Configuration ---
|
|
13
9
|
REMOTE="${1:-origin}"
|
|
14
10
|
BRANCH="${2:-main}"
|
|
15
|
-
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
16
|
-
|
|
17
|
-
# Determine repo directory: script is at src/scripts/, repo root is ../../
|
|
18
11
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
PACKAGE_JSON="$REPO_DIR/package.json"
|
|
22
|
-
|
|
23
|
-
# --- Helpers ---
|
|
24
|
-
RED='\033[0;31m'
|
|
25
|
-
GREEN='\033[0;32m'
|
|
26
|
-
YELLOW='\033[1;33m'
|
|
27
|
-
NC='\033[0m'
|
|
28
|
-
|
|
29
|
-
log() { echo -e "${GREEN}[nexo-update]${NC} $*"; }
|
|
30
|
-
warn() { echo -e "${YELLOW}[nexo-update]${NC} $*"; }
|
|
31
|
-
err() { echo -e "${RED}[nexo-update]${NC} $*" >&2; }
|
|
32
|
-
|
|
33
|
-
read_version() {
|
|
34
|
-
python3 -c "import json; print(json.load(open('$PACKAGE_JSON')).get('version','unknown'))" 2>/dev/null || echo "unknown"
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
# --- Check if this is a git repo ---
|
|
38
|
-
if [ ! -d "$REPO_DIR/.git" ] && [ ! -f "$REPO_DIR/.git" ]; then
|
|
39
|
-
err "ABORTED: Not a git repository at $REPO_DIR"
|
|
40
|
-
err "For packaged installs, use: npm update -g nexo-brain"
|
|
41
|
-
exit 1
|
|
42
|
-
fi
|
|
43
|
-
|
|
44
|
-
# --- Step 1: Check for uncommitted changes in entire worktree ---
|
|
45
|
-
log "Checking for uncommitted changes..."
|
|
46
|
-
cd "$REPO_DIR"
|
|
47
|
-
|
|
48
|
-
if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
|
|
49
|
-
err "ABORTED: Uncommitted changes in worktree"
|
|
50
|
-
git status --short
|
|
51
|
-
exit 1
|
|
52
|
-
fi
|
|
53
|
-
log "Working tree clean."
|
|
54
|
-
|
|
55
|
-
# Record current state
|
|
56
|
-
OLD_VERSION="$(read_version)"
|
|
57
|
-
OLD_COMMIT="$(git rev-parse HEAD)"
|
|
58
|
-
REQ_FILE="$SRC_DIR/requirements.txt"
|
|
59
|
-
OLD_REQ_HASH=""
|
|
60
|
-
if [ -f "$REQ_FILE" ]; then
|
|
61
|
-
OLD_REQ_HASH="$(shasum -a 256 "$REQ_FILE" | cut -d' ' -f1)"
|
|
62
|
-
fi
|
|
63
|
-
log "Current: v${OLD_VERSION} (${OLD_COMMIT:0:8})"
|
|
64
|
-
|
|
65
|
-
# --- Step 2: Backup databases ---
|
|
66
|
-
TIMESTAMP="$(date +%Y-%m-%d-%H%M)"
|
|
67
|
-
BACKUP_DIR="$NEXO_HOME/backups/pre-update-$TIMESTAMP"
|
|
68
|
-
|
|
69
|
-
backup_dbs() {
|
|
70
|
-
local found=0
|
|
71
|
-
# Check data/, NEXO_HOME root, and src/ for .db files
|
|
72
|
-
for dir in "$NEXO_HOME/data" "$NEXO_HOME" "$SRC_DIR"; do
|
|
73
|
-
if [ -d "$dir" ]; then
|
|
74
|
-
for db in "$dir"/*.db; do
|
|
75
|
-
[ -f "$db" ] || continue
|
|
76
|
-
found=1
|
|
77
|
-
mkdir -p "$BACKUP_DIR"
|
|
78
|
-
cp "$db" "$BACKUP_DIR/$(basename "$db")"
|
|
79
|
-
log " Backed up: $(basename "$db")"
|
|
80
|
-
done
|
|
81
|
-
fi
|
|
82
|
-
done
|
|
83
|
-
if [ "$found" -eq 0 ]; then
|
|
84
|
-
log " No databases found to backup."
|
|
85
|
-
fi
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
log "Backing up databases..."
|
|
89
|
-
backup_dbs
|
|
12
|
+
SRC_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
13
|
+
RUNTIME_PYTHON="${NEXO_RUNTIME_PYTHON:-python3}"
|
|
90
14
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
PULL_OUTPUT="$(git pull "$REMOTE" "$BRANCH" 2>&1)" || {
|
|
94
|
-
err "git pull failed:"
|
|
95
|
-
err "$PULL_OUTPUT"
|
|
96
|
-
exit 1
|
|
97
|
-
}
|
|
98
|
-
log "$PULL_OUTPUT"
|
|
15
|
+
export NEXO_CODE="${NEXO_CODE:-$SRC_DIR}"
|
|
16
|
+
export PYTHONPATH="$SRC_DIR${PYTHONPATH:+:$PYTHONPATH}"
|
|
99
17
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
exit 0
|
|
103
|
-
fi
|
|
18
|
+
"$RUNTIME_PYTHON" - "$REMOTE" "$BRANCH" <<'PY'
|
|
19
|
+
from __future__ import annotations
|
|
104
20
|
|
|
105
|
-
|
|
106
|
-
NEW_VERSION="$(read_version)"
|
|
107
|
-
log "New version: v${NEW_VERSION}"
|
|
108
|
-
|
|
109
|
-
# --- Step 4b: Reinstall Python dependencies if requirements.txt changed ---
|
|
110
|
-
NEW_REQ_HASH=""
|
|
111
|
-
if [ -f "$REQ_FILE" ]; then
|
|
112
|
-
NEW_REQ_HASH="$(shasum -a 256 "$REQ_FILE" | cut -d' ' -f1)"
|
|
113
|
-
fi
|
|
114
|
-
|
|
115
|
-
DEPS_CHANGED=false
|
|
116
|
-
if [ "$OLD_REQ_HASH" != "$NEW_REQ_HASH" ]; then
|
|
117
|
-
DEPS_CHANGED=true
|
|
118
|
-
fi
|
|
119
|
-
|
|
120
|
-
reinstall_pip_deps() {
|
|
121
|
-
local VENV_PIP="$NEXO_HOME/.venv/bin/pip"
|
|
122
|
-
if [ -f "$REQ_FILE" ]; then
|
|
123
|
-
if [ -x "$VENV_PIP" ]; then
|
|
124
|
-
"$VENV_PIP" install --quiet -r "$REQ_FILE" || return 1
|
|
125
|
-
else
|
|
126
|
-
python3 -m pip install --quiet -r "$REQ_FILE" --break-system-packages 2>/dev/null || return 1
|
|
127
|
-
fi
|
|
128
|
-
fi
|
|
129
|
-
return 0
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if [ "$DEPS_CHANGED" = true ] || [ "$OLD_VERSION" != "$NEW_VERSION" ]; then
|
|
133
|
-
log "Reinstalling Python dependencies..."
|
|
134
|
-
if ! reinstall_pip_deps; then
|
|
135
|
-
err "pip install failed! Rolling back..."
|
|
136
|
-
git reset --hard "$OLD_COMMIT"
|
|
137
|
-
reinstall_pip_deps || warn "pip rollback also had issues"
|
|
138
|
-
if [ -d "$BACKUP_DIR" ]; then
|
|
139
|
-
for db in "$BACKUP_DIR"/*.db; do
|
|
140
|
-
[ -f "$db" ] || continue
|
|
141
|
-
BASENAME="$(basename "$db")"
|
|
142
|
-
for candidate in "$NEXO_HOME/data/$BASENAME" "$NEXO_HOME/$BASENAME" "$SRC_DIR/$BASENAME"; do
|
|
143
|
-
if [ -f "$candidate" ]; then
|
|
144
|
-
cp "$db" "$candidate"
|
|
145
|
-
warn " Restored: $BASENAME"
|
|
146
|
-
break
|
|
147
|
-
fi
|
|
148
|
-
done
|
|
149
|
-
done
|
|
150
|
-
fi
|
|
151
|
-
err "Rolled back to ${OLD_COMMIT:0:8}. Databases restored."
|
|
152
|
-
exit 1
|
|
153
|
-
fi
|
|
154
|
-
log "Python dependencies updated."
|
|
155
|
-
fi
|
|
156
|
-
|
|
157
|
-
# --- Step 5: Run migrations if version changed ---
|
|
158
|
-
if [ "$OLD_VERSION" != "$NEW_VERSION" ]; then
|
|
159
|
-
log "Version changed: ${OLD_VERSION} -> ${NEW_VERSION}"
|
|
160
|
-
log "Running migrations..."
|
|
161
|
-
if ! (cd "$SRC_DIR" && python3 -c "import db; db.init_db()" 2>&1); then
|
|
162
|
-
err "Migration failed! Rolling back..."
|
|
163
|
-
git reset --hard "$OLD_COMMIT"
|
|
164
|
-
# Reinstall pip deps from restored old requirements.txt
|
|
165
|
-
reinstall_pip_deps || warn "pip rollback also had issues"
|
|
166
|
-
# Restore DB backups
|
|
167
|
-
if [ -d "$BACKUP_DIR" ]; then
|
|
168
|
-
for db in "$BACKUP_DIR"/*.db; do
|
|
169
|
-
[ -f "$db" ] || continue
|
|
170
|
-
BASENAME="$(basename "$db")"
|
|
171
|
-
for candidate in "$NEXO_HOME/data/$BASENAME" "$NEXO_HOME/$BASENAME" "$SRC_DIR/$BASENAME"; do
|
|
172
|
-
if [ -f "$candidate" ]; then
|
|
173
|
-
cp "$db" "$candidate"
|
|
174
|
-
warn " Restored: $BASENAME"
|
|
175
|
-
break
|
|
176
|
-
fi
|
|
177
|
-
done
|
|
178
|
-
done
|
|
179
|
-
fi
|
|
180
|
-
err "Rolled back to ${OLD_COMMIT:0:8}. Databases and deps restored."
|
|
181
|
-
exit 1
|
|
182
|
-
fi
|
|
183
|
-
log "Migrations applied."
|
|
184
|
-
else
|
|
185
|
-
log "Version unchanged (${OLD_VERSION}), skipping migrations."
|
|
186
|
-
fi
|
|
187
|
-
|
|
188
|
-
# --- Step 6: Verify import ---
|
|
189
|
-
log "Verifying server.py import..."
|
|
190
|
-
if ! (cd "$SRC_DIR" && python3 -c "import server" 2>&1); then
|
|
191
|
-
err "Import verification failed! Rolling back..."
|
|
192
|
-
git reset --hard "$OLD_COMMIT"
|
|
193
|
-
# Reinstall pip deps from restored old requirements.txt
|
|
194
|
-
reinstall_pip_deps || warn "pip rollback also had issues"
|
|
195
|
-
if [ -d "$BACKUP_DIR" ]; then
|
|
196
|
-
for db in "$BACKUP_DIR"/*.db; do
|
|
197
|
-
[ -f "$db" ] || continue
|
|
198
|
-
BASENAME="$(basename "$db")"
|
|
199
|
-
for candidate in "$NEXO_HOME/data/$BASENAME" "$NEXO_HOME/$BASENAME" "$SRC_DIR/$BASENAME"; do
|
|
200
|
-
if [ -f "$candidate" ]; then
|
|
201
|
-
cp "$db" "$candidate"
|
|
202
|
-
warn " Restored: $BASENAME"
|
|
203
|
-
break
|
|
204
|
-
fi
|
|
205
|
-
done
|
|
206
|
-
done
|
|
207
|
-
fi
|
|
208
|
-
err "Rolled back to ${OLD_COMMIT:0:8}. Databases and deps restored."
|
|
209
|
-
exit 1
|
|
210
|
-
fi
|
|
211
|
-
|
|
212
|
-
# --- Step 7: Sync hooks to NEXO_HOME ---
|
|
213
|
-
HOOKS_SRC="$SRC_DIR/hooks"
|
|
214
|
-
HOOKS_DEST="$NEXO_HOME/hooks"
|
|
215
|
-
if [ -d "$HOOKS_SRC" ]; then
|
|
216
|
-
mkdir -p "$HOOKS_DEST"
|
|
217
|
-
SYNCED=0
|
|
218
|
-
for hook in "$HOOKS_SRC"/*.sh; do
|
|
219
|
-
[ -f "$hook" ] || continue
|
|
220
|
-
cp "$hook" "$HOOKS_DEST/$(basename "$hook")"
|
|
221
|
-
chmod 755 "$HOOKS_DEST/$(basename "$hook")"
|
|
222
|
-
SYNCED=$((SYNCED + 1))
|
|
223
|
-
done
|
|
224
|
-
if [ "$SYNCED" -gt 0 ]; then
|
|
225
|
-
log "Synced $SYNCED hook(s) to $HOOKS_DEST"
|
|
226
|
-
fi
|
|
227
|
-
fi
|
|
21
|
+
import sys
|
|
228
22
|
|
|
229
|
-
|
|
230
|
-
CRON_SYNC="$SRC_DIR/crons/sync.py"
|
|
231
|
-
CRON_SYNC_OK=false
|
|
232
|
-
if [ -f "$CRON_SYNC" ]; then
|
|
233
|
-
log "Syncing cron definitions..."
|
|
234
|
-
if NEXO_HOME="$NEXO_HOME" NEXO_CODE="$SRC_DIR" python3 "$CRON_SYNC" 2>&1; then
|
|
235
|
-
log "Cron definitions synced."
|
|
236
|
-
CRON_SYNC_OK=true
|
|
237
|
-
else
|
|
238
|
-
warn "Cron sync failed (non-fatal). Installed manifest NOT refreshed to avoid divergence."
|
|
239
|
-
fi
|
|
240
|
-
fi
|
|
23
|
+
from plugins.update import handle_update
|
|
241
24
|
|
|
242
|
-
# --- Step 8b: Refresh installed manifest for catchup/watchdog (only if sync succeeded) ---
|
|
243
|
-
if $CRON_SYNC_OK && [ -d "$SRC_DIR/crons" ]; then
|
|
244
|
-
mkdir -p "$NEXO_HOME/crons"
|
|
245
|
-
cp -f "$SRC_DIR/crons/"*.json "$NEXO_HOME/crons/" 2>/dev/null
|
|
246
|
-
cp -f "$SRC_DIR/crons/"*.py "$NEXO_HOME/crons/" 2>/dev/null
|
|
247
|
-
log "Refreshed installed crons manifest."
|
|
248
|
-
fi
|
|
249
25
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
CLIENT_SYNC_ARGS=()
|
|
255
|
-
if [ -f "$NEXO_HOME/config/schedule.json" ]; then
|
|
256
|
-
while IFS= read -r line; do
|
|
257
|
-
[ -n "$line" ] && CLIENT_SYNC_ARGS+=("--enabled-client" "$line")
|
|
258
|
-
done < <(
|
|
259
|
-
NEXO_HOME="$NEXO_HOME" NEXO_CODE="$SRC_DIR" python3 - <<'PY'
|
|
260
|
-
import json
|
|
261
|
-
import os
|
|
262
|
-
import sys
|
|
263
|
-
from pathlib import Path
|
|
26
|
+
remote = sys.argv[1]
|
|
27
|
+
branch = sys.argv[2]
|
|
28
|
+
result = handle_update(remote=remote, branch=branch)
|
|
29
|
+
print(result)
|
|
264
30
|
|
|
265
|
-
|
|
266
|
-
from client_preferences import normalize_client_preferences
|
|
267
|
-
|
|
268
|
-
schedule_file = Path(os.environ["NEXO_HOME"]) / "config" / "schedule.json"
|
|
269
|
-
schedule_payload = json.loads(schedule_file.read_text()) if schedule_file.exists() else {}
|
|
270
|
-
prefs = normalize_client_preferences(schedule_payload)
|
|
271
|
-
if prefs != {key: schedule_payload.get(key) for key in prefs}:
|
|
272
|
-
merged = dict(schedule_payload)
|
|
273
|
-
merged.update(prefs)
|
|
274
|
-
schedule_file.parent.mkdir(parents=True, exist_ok=True)
|
|
275
|
-
schedule_file.write_text(json.dumps(merged, indent=2, ensure_ascii=False) + "\n")
|
|
276
|
-
enabled = [key for key, value in prefs.get("interactive_clients", {}).items() if value]
|
|
277
|
-
if prefs.get("automation_enabled", True):
|
|
278
|
-
backend = prefs.get("automation_backend")
|
|
279
|
-
if backend and backend != "none" and backend not in enabled:
|
|
280
|
-
enabled.append(backend)
|
|
281
|
-
for item in enabled:
|
|
282
|
-
print(item)
|
|
31
|
+
raise SystemExit(1 if result.startswith(("ABORTED", "UPDATE FAILED")) else 0)
|
|
283
32
|
PY
|
|
284
|
-
)
|
|
285
|
-
fi
|
|
286
|
-
if NEXO_HOME="$NEXO_HOME" NEXO_CODE="$SRC_DIR" python3 "$CLIENT_SYNC" --nexo-home "$NEXO_HOME" --runtime-root "$SRC_DIR" "${CLIENT_SYNC_ARGS[@]}" --json >/dev/null 2>&1; then
|
|
287
|
-
log "Shared client configs synced."
|
|
288
|
-
else
|
|
289
|
-
warn "Client config sync failed (non-fatal). Run 'nexo clients sync' later."
|
|
290
|
-
fi
|
|
291
|
-
fi
|
|
292
|
-
|
|
293
|
-
# --- Done ---
|
|
294
|
-
echo ""
|
|
295
|
-
log "========================================="
|
|
296
|
-
log " UPDATE SUCCESSFUL"
|
|
297
|
-
if [ "$OLD_VERSION" != "$NEW_VERSION" ]; then
|
|
298
|
-
log " Version: ${OLD_VERSION} -> ${NEW_VERSION}"
|
|
299
|
-
else
|
|
300
|
-
log " Version: ${OLD_VERSION} (unchanged)"
|
|
301
|
-
fi
|
|
302
|
-
log " Branch: ${REMOTE}/${BRANCH}"
|
|
303
|
-
log " Backup: ${BACKUP_DIR}"
|
|
304
|
-
log "========================================="
|
|
305
|
-
echo ""
|
|
306
|
-
warn "MCP server restart needed to load new code."
|