orchestrix 15.14.0
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/bin/o8x.js +91 -0
- package/lib/doctor.js +150 -0
- package/lib/embedded/commands.js +163 -0
- package/lib/embedded/config.js +74 -0
- package/lib/embedded/handoff-detector.sh +374 -0
- package/lib/embedded/hooks.js +21 -0
- package/lib/embedded/scripts.js +24 -0
- package/lib/embedded/start-orchestrix.sh +373 -0
- package/lib/install.js +240 -0
- package/lib/license.js +79 -0
- package/lib/mcp-client.js +111 -0
- package/lib/merge.js +200 -0
- package/lib/ui.js +109 -0
- package/lib/uninstall.js +57 -0
- package/lib/upgrade.js +19 -0
- package/package.json +31 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Orchestrix tmux Multi-Agent Session Starter (MCP Version)
|
|
3
|
+
# Purpose: Create 4 separate windows, each running a Claude Code agent
|
|
4
|
+
# Supports multi-repo: Each repo gets its own tmux session based on repository_id
|
|
5
|
+
#
|
|
6
|
+
# Pro/Team Feature: This script is only available for Pro and Team subscribers.
|
|
7
|
+
|
|
8
|
+
set -e
|
|
9
|
+
|
|
10
|
+
# Dynamically get project root directory (where .orchestrix-core is located)
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
12
|
+
WORK_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
13
|
+
|
|
14
|
+
echo "Working directory: $WORK_DIR"
|
|
15
|
+
|
|
16
|
+
# ============================================
|
|
17
|
+
# Dynamic Session Naming (Multi-Repo Support)
|
|
18
|
+
# ============================================
|
|
19
|
+
|
|
20
|
+
# Try to read repository_id from core-config.yaml
|
|
21
|
+
CONFIG_FILE="$WORK_DIR/.orchestrix-core/core-config.yaml"
|
|
22
|
+
REPO_ID=""
|
|
23
|
+
|
|
24
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
25
|
+
# Extract repository_id using grep and sed (POSIX compatible)
|
|
26
|
+
REPO_ID=$(grep -E "^\s*repository_id:" "$CONFIG_FILE" 2>/dev/null | head -1 | sed "s/.*repository_id:[[:space:]]*['\"]*//" | sed "s/['\"].*//")
|
|
27
|
+
|
|
28
|
+
# Clean up: remove quotes and whitespace
|
|
29
|
+
REPO_ID=$(echo "$REPO_ID" | tr -d "'" | tr -d '"' | tr -d ' ')
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Fallback: use directory name if repository_id is empty
|
|
33
|
+
if [ -z "$REPO_ID" ]; then
|
|
34
|
+
REPO_ID=$(basename "$WORK_DIR")
|
|
35
|
+
echo "Warning: No repository_id in config, using directory name: $REPO_ID"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Sanitize REPO_ID for tmux session name (alphanumeric, dash, underscore only)
|
|
39
|
+
REPO_ID=$(echo "$REPO_ID" | tr -cd 'a-zA-Z0-9_-')
|
|
40
|
+
|
|
41
|
+
# Sanitized REPO_ID must not be empty
|
|
42
|
+
if [ -z "$REPO_ID" ]; then
|
|
43
|
+
REPO_ID="default"
|
|
44
|
+
echo "⚠️ REPO_ID is empty after sanitization, using fallback: $REPO_ID"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Generate dynamic session name and log file
|
|
48
|
+
SESSION_NAME="orchestrix-${REPO_ID}"
|
|
49
|
+
# IMPORTANT: Must match handoff-detector.sh pattern: /tmp/${SESSION_NAME}-handoff.log
|
|
50
|
+
LOG_FILE="/tmp/${SESSION_NAME}-handoff.log"
|
|
51
|
+
|
|
52
|
+
echo "🏷️ Repository ID: $REPO_ID"
|
|
53
|
+
echo "📺 tmux Session: $SESSION_NAME"
|
|
54
|
+
echo "📝 Log file: $LOG_FILE"
|
|
55
|
+
|
|
56
|
+
# Check if tmux is installed
|
|
57
|
+
if ! command -v tmux &> /dev/null; then
|
|
58
|
+
echo "❌ Error: tmux is not installed"
|
|
59
|
+
echo "Please run: brew install tmux"
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# Check if cc command is available
|
|
64
|
+
if ! command -v cc &> /dev/null; then
|
|
65
|
+
echo "❌ Error: cc command not available"
|
|
66
|
+
echo "Please ensure Claude Code alias is configured: alias cc='claude'"
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# If session already exists, kill it first
|
|
71
|
+
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
|
|
72
|
+
echo "⚠️ Session '$SESSION_NAME' already exists, closing..."
|
|
73
|
+
tmux kill-session -t "$SESSION_NAME"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# Create new session with first window (Architect)
|
|
77
|
+
echo "🚀 Creating tmux session: $SESSION_NAME"
|
|
78
|
+
tmux new-session -d -s "$SESSION_NAME" -n "Arch" -c "$WORK_DIR"
|
|
79
|
+
|
|
80
|
+
# ============================================
|
|
81
|
+
# Inject handoff-detector hook into target project
|
|
82
|
+
# ============================================
|
|
83
|
+
# CRITICAL: The tmux agents run Claude Code in WORK_DIR, so their Stop hooks
|
|
84
|
+
# come from WORK_DIR/.claude/settings*.json, NOT from the orchestrix-mcp-server project.
|
|
85
|
+
# We must inject the handoff-detector hook into the target project's settings.local.json
|
|
86
|
+
# so that agent Stop events trigger local handoff detection.
|
|
87
|
+
SETTINGS_LOCAL="$WORK_DIR/.claude/settings.local.json"
|
|
88
|
+
HANDOFF_HOOK_CMD="bash -c 'cd \"\$(git rev-parse --show-toplevel)\" && .orchestrix-core/scripts/handoff-detector.sh'"
|
|
89
|
+
|
|
90
|
+
# Ensure .claude directory exists
|
|
91
|
+
mkdir -p "$WORK_DIR/.claude"
|
|
92
|
+
|
|
93
|
+
# Check if settings.local.json already has handoff-detector configured
|
|
94
|
+
if [ -f "$SETTINGS_LOCAL" ]; then
|
|
95
|
+
if grep -q "handoff-detector" "$SETTINGS_LOCAL" 2>/dev/null; then
|
|
96
|
+
echo "✅ Handoff hook already configured in $SETTINGS_LOCAL"
|
|
97
|
+
else
|
|
98
|
+
echo "⚠️ settings.local.json exists but missing handoff hook, injecting..."
|
|
99
|
+
# Use jq to merge if available, otherwise create new
|
|
100
|
+
if command -v jq &>/dev/null; then
|
|
101
|
+
EXISTING=$(cat "$SETTINGS_LOCAL")
|
|
102
|
+
echo "$EXISTING" | jq --arg cmd "$HANDOFF_HOOK_CMD" \
|
|
103
|
+
'.hooks.Stop = (.hooks.Stop // []) + [{"hooks": [{"type": "command", "command": $cmd}]}]' \
|
|
104
|
+
> "$SETTINGS_LOCAL.tmp" && mv "$SETTINGS_LOCAL.tmp" "$SETTINGS_LOCAL"
|
|
105
|
+
echo "✅ Handoff hook injected into existing settings.local.json"
|
|
106
|
+
else
|
|
107
|
+
echo "⚠️ jq not found, cannot safely merge. Please add handoff hook manually."
|
|
108
|
+
fi
|
|
109
|
+
fi
|
|
110
|
+
else
|
|
111
|
+
# Create new settings.local.json with handoff hook
|
|
112
|
+
cat > "$SETTINGS_LOCAL" << SETTINGS_EOF
|
|
113
|
+
{
|
|
114
|
+
"hooks": {
|
|
115
|
+
"Stop": [
|
|
116
|
+
{
|
|
117
|
+
"hooks": [
|
|
118
|
+
{
|
|
119
|
+
"type": "command",
|
|
120
|
+
"command": "$HANDOFF_HOOK_CMD"
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
SETTINGS_EOF
|
|
128
|
+
echo "✅ Created $SETTINGS_LOCAL with handoff hook"
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# ============================================
|
|
132
|
+
# Ensure handoff-detector.sh exists in target project
|
|
133
|
+
# ============================================
|
|
134
|
+
HANDOFF_SCRIPT="$WORK_DIR/.orchestrix-core/scripts/handoff-detector.sh"
|
|
135
|
+
if [ ! -f "$HANDOFF_SCRIPT" ]; then
|
|
136
|
+
SCRIPT_DIR_SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
137
|
+
if [ -f "$SCRIPT_DIR_SRC/handoff-detector.sh" ]; then
|
|
138
|
+
mkdir -p "$(dirname "$HANDOFF_SCRIPT")"
|
|
139
|
+
cp "$SCRIPT_DIR_SRC/handoff-detector.sh" "$HANDOFF_SCRIPT"
|
|
140
|
+
chmod +x "$HANDOFF_SCRIPT"
|
|
141
|
+
echo "✅ Copied handoff-detector.sh to target project"
|
|
142
|
+
else
|
|
143
|
+
echo "⚠️ handoff-detector.sh not found in setup directory"
|
|
144
|
+
fi
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
# ============================================
|
|
148
|
+
# Create tmux automation marker
|
|
149
|
+
# ============================================
|
|
150
|
+
# This file signals to agents that they're running in tmux automation mode
|
|
151
|
+
# Agents check for this file to decide whether to register pending-handoff fallback
|
|
152
|
+
RUNTIME_DIR="$WORK_DIR/.orchestrix-core/runtime"
|
|
153
|
+
TMUX_MARKER="$RUNTIME_DIR/tmux-automation-active"
|
|
154
|
+
|
|
155
|
+
mkdir -p "$RUNTIME_DIR"
|
|
156
|
+
echo "{\"session\": \"$SESSION_NAME\", \"started_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" > "$TMUX_MARKER"
|
|
157
|
+
echo "📌 tmux automation marker: $TMUX_MARKER"
|
|
158
|
+
|
|
159
|
+
# Cleanup marker file on script exit (detach, kill, Ctrl+C)
|
|
160
|
+
cleanup() {
|
|
161
|
+
rm -f "$TMUX_MARKER"
|
|
162
|
+
echo "🧹 Cleaned up tmux automation marker"
|
|
163
|
+
}
|
|
164
|
+
trap cleanup EXIT INT TERM
|
|
165
|
+
|
|
166
|
+
# Configure status bar for better display
|
|
167
|
+
tmux set-option -t "$SESSION_NAME" status-left-length 20
|
|
168
|
+
tmux set-option -t "$SESSION_NAME" status-right-length 60
|
|
169
|
+
tmux set-option -t "$SESSION_NAME" window-status-format "#I:#W"
|
|
170
|
+
tmux set-option -t "$SESSION_NAME" window-status-current-format "#I:#W*"
|
|
171
|
+
|
|
172
|
+
# Set environment variables for Architect window
|
|
173
|
+
# ORCHESTRIX_SESSION and ORCHESTRIX_LOG are used by handoff-detector.sh
|
|
174
|
+
tmux send-keys -t "$SESSION_NAME:0" "export AGENT_ID=architect" C-m
|
|
175
|
+
tmux send-keys -t "$SESSION_NAME:0" "export ORCHESTRIX_SESSION=$SESSION_NAME" C-m
|
|
176
|
+
tmux send-keys -t "$SESSION_NAME:0" "export ORCHESTRIX_LOG=$LOG_FILE" C-m
|
|
177
|
+
tmux send-keys -t "$SESSION_NAME:0" "clear" C-m
|
|
178
|
+
tmux send-keys -t "$SESSION_NAME:0" "echo '╔════════════════════════════════════════╗'" C-m
|
|
179
|
+
tmux send-keys -t "$SESSION_NAME:0" "echo '║ 🏛️ Architect Agent (Window 0) ║'" C-m
|
|
180
|
+
tmux send-keys -t "$SESSION_NAME:0" "echo '╚════════════════════════════════════════╝'" C-m
|
|
181
|
+
tmux send-keys -t "$SESSION_NAME:0" "echo ''" C-m
|
|
182
|
+
|
|
183
|
+
# Create window 1 - SM
|
|
184
|
+
tmux new-window -t "$SESSION_NAME:1" -n "SM" -c "$WORK_DIR"
|
|
185
|
+
tmux send-keys -t "$SESSION_NAME:1" "export AGENT_ID=sm" C-m
|
|
186
|
+
tmux send-keys -t "$SESSION_NAME:1" "export ORCHESTRIX_SESSION=$SESSION_NAME" C-m
|
|
187
|
+
tmux send-keys -t "$SESSION_NAME:1" "export ORCHESTRIX_LOG=$LOG_FILE" C-m
|
|
188
|
+
tmux send-keys -t "$SESSION_NAME:1" "clear" C-m
|
|
189
|
+
tmux send-keys -t "$SESSION_NAME:1" "echo '╔════════════════════════════════════════╗'" C-m
|
|
190
|
+
tmux send-keys -t "$SESSION_NAME:1" "echo '║ 📋 SM Agent (Window 1) ║'" C-m
|
|
191
|
+
tmux send-keys -t "$SESSION_NAME:1" "echo '╚════════════════════════════════════════╝'" C-m
|
|
192
|
+
tmux send-keys -t "$SESSION_NAME:1" "echo ''" C-m
|
|
193
|
+
|
|
194
|
+
# Create window 2 - Dev
|
|
195
|
+
tmux new-window -t "$SESSION_NAME:2" -n "Dev" -c "$WORK_DIR"
|
|
196
|
+
tmux send-keys -t "$SESSION_NAME:2" "export AGENT_ID=dev" C-m
|
|
197
|
+
tmux send-keys -t "$SESSION_NAME:2" "export ORCHESTRIX_SESSION=$SESSION_NAME" C-m
|
|
198
|
+
tmux send-keys -t "$SESSION_NAME:2" "export ORCHESTRIX_LOG=$LOG_FILE" C-m
|
|
199
|
+
tmux send-keys -t "$SESSION_NAME:2" "clear" C-m
|
|
200
|
+
tmux send-keys -t "$SESSION_NAME:2" "echo '╔════════════════════════════════════════╗'" C-m
|
|
201
|
+
tmux send-keys -t "$SESSION_NAME:2" "echo '║ 💻 Dev Agent (Window 2) ║'" C-m
|
|
202
|
+
tmux send-keys -t "$SESSION_NAME:2" "echo '╚════════════════════════════════════════╝'" C-m
|
|
203
|
+
tmux send-keys -t "$SESSION_NAME:2" "echo ''" C-m
|
|
204
|
+
|
|
205
|
+
# Create window 3 - QA
|
|
206
|
+
tmux new-window -t "$SESSION_NAME:3" -n "QA" -c "$WORK_DIR"
|
|
207
|
+
tmux send-keys -t "$SESSION_NAME:3" "export AGENT_ID=qa" C-m
|
|
208
|
+
tmux send-keys -t "$SESSION_NAME:3" "export ORCHESTRIX_SESSION=$SESSION_NAME" C-m
|
|
209
|
+
tmux send-keys -t "$SESSION_NAME:3" "export ORCHESTRIX_LOG=$LOG_FILE" C-m
|
|
210
|
+
tmux send-keys -t "$SESSION_NAME:3" "clear" C-m
|
|
211
|
+
tmux send-keys -t "$SESSION_NAME:3" "echo '╔════════════════════════════════════════╗'" C-m
|
|
212
|
+
tmux send-keys -t "$SESSION_NAME:3" "echo '║ 🧪 QA Agent (Window 3) ║'" C-m
|
|
213
|
+
tmux send-keys -t "$SESSION_NAME:3" "echo '╚════════════════════════════════════════╝'" C-m
|
|
214
|
+
tmux send-keys -t "$SESSION_NAME:3" "echo ''" C-m
|
|
215
|
+
|
|
216
|
+
# ============================================
|
|
217
|
+
# Configuration
|
|
218
|
+
# ============================================
|
|
219
|
+
|
|
220
|
+
# Wait time for Claude Code to start (seconds)
|
|
221
|
+
CC_STARTUP_WAIT=12
|
|
222
|
+
|
|
223
|
+
# Wait time between command text and Enter key (seconds)
|
|
224
|
+
COMMAND_ENTER_DELAY=1
|
|
225
|
+
|
|
226
|
+
# Wait time between activating agents (seconds)
|
|
227
|
+
AGENT_ACTIVATION_DELAY=2
|
|
228
|
+
|
|
229
|
+
# Wait time for agents to fully load before starting workflow (seconds)
|
|
230
|
+
AGENT_LOAD_WAIT=15
|
|
231
|
+
|
|
232
|
+
# Auto-start workflow command (sent to SM window)
|
|
233
|
+
AUTO_START_COMMAND="1"
|
|
234
|
+
|
|
235
|
+
# Agent activation commands (MCP version uses /o command)
|
|
236
|
+
declare -a AGENT_COMMANDS=(
|
|
237
|
+
"/o architect" # Window 0 - Architect
|
|
238
|
+
"/o sm" # Window 1 - SM
|
|
239
|
+
"/o dev" # Window 2 - Dev
|
|
240
|
+
"/o qa" # Window 3 - QA
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
declare -a AGENT_NAMES=(
|
|
244
|
+
"Architect"
|
|
245
|
+
"SM"
|
|
246
|
+
"Dev"
|
|
247
|
+
"QA"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# ============================================
|
|
251
|
+
# Function: Send command with delay before Enter
|
|
252
|
+
# Usage: send_command_with_delay <window> <command>
|
|
253
|
+
# ============================================
|
|
254
|
+
send_command_with_delay() {
|
|
255
|
+
local window="$1"
|
|
256
|
+
local command="$2"
|
|
257
|
+
|
|
258
|
+
# Send command text first
|
|
259
|
+
tmux send-keys -t "$SESSION_NAME:$window" "$command"
|
|
260
|
+
|
|
261
|
+
# Wait before sending Enter (prevents race condition)
|
|
262
|
+
sleep "$COMMAND_ENTER_DELAY"
|
|
263
|
+
|
|
264
|
+
# Send Enter key
|
|
265
|
+
tmux send-keys -t "$SESSION_NAME:$window" "Enter"
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
# ============================================
|
|
269
|
+
# Start Claude Code in all windows
|
|
270
|
+
# ============================================
|
|
271
|
+
|
|
272
|
+
echo "🤖 Starting Claude Code in all windows..."
|
|
273
|
+
|
|
274
|
+
# Window 0 - Architect
|
|
275
|
+
tmux send-keys -t "$SESSION_NAME:0" "cc" C-m
|
|
276
|
+
|
|
277
|
+
# Window 1 - SM
|
|
278
|
+
tmux send-keys -t "$SESSION_NAME:1" "cc" C-m
|
|
279
|
+
|
|
280
|
+
# Window 2 - Dev
|
|
281
|
+
tmux send-keys -t "$SESSION_NAME:2" "cc" C-m
|
|
282
|
+
|
|
283
|
+
# Window 3 - QA
|
|
284
|
+
tmux send-keys -t "$SESSION_NAME:3" "cc" C-m
|
|
285
|
+
|
|
286
|
+
# ============================================
|
|
287
|
+
# Wait for Claude Code to fully initialize
|
|
288
|
+
# ============================================
|
|
289
|
+
|
|
290
|
+
echo ""
|
|
291
|
+
echo "⏳ Waiting ${CC_STARTUP_WAIT}s for Claude Code to start..."
|
|
292
|
+
echo ""
|
|
293
|
+
|
|
294
|
+
# Show countdown
|
|
295
|
+
for i in $(seq "$CC_STARTUP_WAIT" -1 1); do
|
|
296
|
+
printf "\r %2d seconds remaining..." "$i"
|
|
297
|
+
sleep 1
|
|
298
|
+
done
|
|
299
|
+
printf "\r ✓ Claude Code should be ready now! \n"
|
|
300
|
+
echo ""
|
|
301
|
+
|
|
302
|
+
# ============================================
|
|
303
|
+
# Auto-activate agents in each window
|
|
304
|
+
# ============================================
|
|
305
|
+
|
|
306
|
+
echo "🚀 Auto-activating agents..."
|
|
307
|
+
echo ""
|
|
308
|
+
|
|
309
|
+
for window in 0 1 2 3; do
|
|
310
|
+
agent_name="${AGENT_NAMES[$window]}"
|
|
311
|
+
agent_cmd="${AGENT_COMMANDS[$window]}"
|
|
312
|
+
|
|
313
|
+
echo " [Window $window] Activating $agent_name..."
|
|
314
|
+
send_command_with_delay "$window" "$agent_cmd"
|
|
315
|
+
|
|
316
|
+
# Wait before activating next agent (avoid overwhelming the system)
|
|
317
|
+
if [ "$window" -lt 3 ]; then
|
|
318
|
+
sleep "$AGENT_ACTIVATION_DELAY"
|
|
319
|
+
fi
|
|
320
|
+
done
|
|
321
|
+
|
|
322
|
+
echo ""
|
|
323
|
+
echo "✅ All agents activated!"
|
|
324
|
+
|
|
325
|
+
# ============================================
|
|
326
|
+
# Wait for agents to fully load
|
|
327
|
+
# ============================================
|
|
328
|
+
|
|
329
|
+
echo ""
|
|
330
|
+
echo "⏳ Waiting ${AGENT_LOAD_WAIT}s for agents to load..."
|
|
331
|
+
echo ""
|
|
332
|
+
|
|
333
|
+
for i in $(seq "$AGENT_LOAD_WAIT" -1 1); do
|
|
334
|
+
printf "\r %2d seconds remaining..." "$i"
|
|
335
|
+
sleep 1
|
|
336
|
+
done
|
|
337
|
+
printf "\r ✓ Agents should be ready now! \n"
|
|
338
|
+
|
|
339
|
+
# ============================================
|
|
340
|
+
# Auto-start workflow in SM window
|
|
341
|
+
# ============================================
|
|
342
|
+
|
|
343
|
+
echo ""
|
|
344
|
+
echo "🎬 Starting workflow in SM window..."
|
|
345
|
+
send_command_with_delay "1" "$AUTO_START_COMMAND"
|
|
346
|
+
|
|
347
|
+
# Select SM window (window 1) as starting point
|
|
348
|
+
tmux select-window -t "$SESSION_NAME:1"
|
|
349
|
+
|
|
350
|
+
# Display startup completion message
|
|
351
|
+
echo ""
|
|
352
|
+
echo "═══════════════════════════════════════════════════════════════"
|
|
353
|
+
echo "✅ Orchestrix automation started!"
|
|
354
|
+
echo "═══════════════════════════════════════════════════════════════"
|
|
355
|
+
echo ""
|
|
356
|
+
echo "📋 Window Layout:"
|
|
357
|
+
echo " Window 0: 🏛️ Architect"
|
|
358
|
+
echo " Window 1: 📋 SM (current window) ← workflow started"
|
|
359
|
+
echo " Window 2: 💻 Dev"
|
|
360
|
+
echo " Window 3: 🧪 QA"
|
|
361
|
+
echo ""
|
|
362
|
+
echo "⌨️ tmux navigation:"
|
|
363
|
+
echo " Ctrl+b → 0/1/2/3 Jump to window"
|
|
364
|
+
echo " Ctrl+b → n/p Next/Previous window"
|
|
365
|
+
echo " Ctrl+b → d Detach (runs in background)"
|
|
366
|
+
echo " Ctrl+b → [ Scroll mode (q to exit)"
|
|
367
|
+
echo ""
|
|
368
|
+
echo "📝 Monitor: tail -f $LOG_FILE"
|
|
369
|
+
echo "📝 Reconnect: tmux attach -t $SESSION_NAME"
|
|
370
|
+
echo ""
|
|
371
|
+
|
|
372
|
+
# Attach to session
|
|
373
|
+
tmux attach-session -t "$SESSION_NAME"
|
package/lib/install.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const ui = require('./ui');
|
|
7
|
+
const { resolveKey, writeKeyToEnvLocal } = require('./license');
|
|
8
|
+
const { mergeMcpJson, mergeSettingsLocal } = require('./merge');
|
|
9
|
+
const { validateKey } = require('./mcp-client');
|
|
10
|
+
const commands = require('./embedded/commands');
|
|
11
|
+
const { CORE_CONFIG_TEMPLATE } = require('./embedded/config');
|
|
12
|
+
const scripts = require('./embedded/scripts');
|
|
13
|
+
|
|
14
|
+
const TOTAL_STEPS = 5;
|
|
15
|
+
|
|
16
|
+
async function install(flags) {
|
|
17
|
+
ui.banner();
|
|
18
|
+
const projectDir = process.cwd();
|
|
19
|
+
|
|
20
|
+
// ────────────────────────────────────────
|
|
21
|
+
// Phase 1: Pre-flight
|
|
22
|
+
// ────────────────────────────────────────
|
|
23
|
+
ui.step(1, TOTAL_STEPS, 'Pre-flight checks');
|
|
24
|
+
|
|
25
|
+
// Check Node version
|
|
26
|
+
const nodeVersion = parseInt(process.versions.node.split('.')[0], 10);
|
|
27
|
+
if (nodeVersion < 18) {
|
|
28
|
+
throw new Error(`Node.js >= 18 required (current: ${process.versions.node})`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check git repo
|
|
32
|
+
let isGitRepo = false;
|
|
33
|
+
let repoName = path.basename(projectDir);
|
|
34
|
+
try {
|
|
35
|
+
execSync('git rev-parse --show-toplevel', { stdio: 'pipe', cwd: projectDir });
|
|
36
|
+
isGitRepo = true;
|
|
37
|
+
repoName = path.basename(execSync('git rev-parse --show-toplevel', { stdio: 'pipe', cwd: projectDir }).toString().trim());
|
|
38
|
+
} catch {
|
|
39
|
+
ui.warn('Not a git repository — proceeding anyway');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Detect existing installation
|
|
43
|
+
const hasExisting = fs.existsSync(path.join(projectDir, '.orchestrix-core'));
|
|
44
|
+
if (hasExisting && !flags.force) {
|
|
45
|
+
ui.info('Existing Orchestrix installation detected — will upgrade');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ui.success(`Project: ${repoName}`);
|
|
49
|
+
|
|
50
|
+
// ────────────────────────────────────────
|
|
51
|
+
// Phase 2: License Key
|
|
52
|
+
// ────────────────────────────────────────
|
|
53
|
+
ui.step(2, TOTAL_STEPS, 'License key');
|
|
54
|
+
|
|
55
|
+
let licenseKey;
|
|
56
|
+
if (flags.offline) {
|
|
57
|
+
ui.info('Offline mode — skipping license validation');
|
|
58
|
+
licenseKey = flags.key || process.env.ORCHESTRIX_LICENSE_KEY || '';
|
|
59
|
+
} else {
|
|
60
|
+
licenseKey = await resolveKey(flags);
|
|
61
|
+
|
|
62
|
+
// Validate key
|
|
63
|
+
const validation = await validateKey(licenseKey);
|
|
64
|
+
if (validation.valid) {
|
|
65
|
+
ui.success(`License valid (tier: ${validation.tier})`);
|
|
66
|
+
} else {
|
|
67
|
+
ui.warn(`License validation failed: ${validation.error}`);
|
|
68
|
+
ui.info('Continuing with embedded files (offline mode)');
|
|
69
|
+
flags.offline = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ────────────────────────────────────────
|
|
74
|
+
// Phase 3: Write slash commands
|
|
75
|
+
// ────────────────────────────────────────
|
|
76
|
+
ui.step(3, TOTAL_STEPS, 'Installing files');
|
|
77
|
+
console.log();
|
|
78
|
+
|
|
79
|
+
// Ensure directories
|
|
80
|
+
const dirs = [
|
|
81
|
+
path.join(projectDir, '.claude', 'commands'),
|
|
82
|
+
path.join(projectDir, '.orchestrix-core', 'scripts'),
|
|
83
|
+
path.join(projectDir, '.orchestrix-core', 'runtime'),
|
|
84
|
+
];
|
|
85
|
+
for (const dir of dirs) {
|
|
86
|
+
if (!fs.existsSync(dir)) {
|
|
87
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Write slash commands (always overwrite — these are Orchestrix-owned)
|
|
92
|
+
for (const [filename, content] of Object.entries(commands)) {
|
|
93
|
+
const filePath = path.join(projectDir, '.claude', 'commands', filename);
|
|
94
|
+
fs.writeFileSync(filePath, content);
|
|
95
|
+
ui.fileAction(fs.existsSync(filePath) ? 'overwrite' : 'create', `.claude/commands/${filename}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ────────────────────────────────────────
|
|
99
|
+
// Phase 4: MCP config, scripts, hooks
|
|
100
|
+
// ────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
// .mcp.json
|
|
103
|
+
if (!flags.noMcp) {
|
|
104
|
+
const mcpAction = mergeMcpJson(projectDir);
|
|
105
|
+
ui.fileAction(mcpAction, '.mcp.json');
|
|
106
|
+
} else {
|
|
107
|
+
ui.fileAction('skip', '.mcp.json (--no-mcp)');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// core-config.yaml (only create if missing)
|
|
111
|
+
const configPath = path.join(projectDir, '.orchestrix-core', 'core-config.yaml');
|
|
112
|
+
if (!fs.existsSync(configPath)) {
|
|
113
|
+
// Substitute placeholders
|
|
114
|
+
let config = CORE_CONFIG_TEMPLATE
|
|
115
|
+
.replace(/\{\{PROJECT_NAME\}\}/g, repoName)
|
|
116
|
+
.replace(/\{\{REPO_ID\}\}/g, repoName)
|
|
117
|
+
.replace(/\{\{TEST_COMMAND\}\}/g, detectTestCommand(projectDir));
|
|
118
|
+
fs.writeFileSync(configPath, config);
|
|
119
|
+
ui.fileAction('create', '.orchestrix-core/core-config.yaml');
|
|
120
|
+
} else {
|
|
121
|
+
ui.fileAction('skip', '.orchestrix-core/core-config.yaml (exists)');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// tmux scripts (Pro feature, always install from embedded)
|
|
125
|
+
if (!flags.noScripts) {
|
|
126
|
+
try {
|
|
127
|
+
const startScript = scripts.getStartScript();
|
|
128
|
+
const handoffScript = scripts.getHandoffScript();
|
|
129
|
+
|
|
130
|
+
const startPath = path.join(projectDir, '.orchestrix-core', 'scripts', 'start-orchestrix.sh');
|
|
131
|
+
const handoffPath = path.join(projectDir, '.orchestrix-core', 'scripts', 'handoff-detector.sh');
|
|
132
|
+
|
|
133
|
+
fs.writeFileSync(startPath, startScript);
|
|
134
|
+
fs.chmodSync(startPath, 0o755);
|
|
135
|
+
ui.fileAction('create', '.orchestrix-core/scripts/start-orchestrix.sh');
|
|
136
|
+
|
|
137
|
+
fs.writeFileSync(handoffPath, handoffScript);
|
|
138
|
+
fs.chmodSync(handoffPath, 0o755);
|
|
139
|
+
ui.fileAction('create', '.orchestrix-core/scripts/handoff-detector.sh');
|
|
140
|
+
} catch (err) {
|
|
141
|
+
ui.warn(`Scripts: ${err.message}`);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
ui.fileAction('skip', '.orchestrix-core/scripts/ (--no-scripts)');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Hooks (settings.local.json)
|
|
148
|
+
if (!flags.noHooks) {
|
|
149
|
+
const hookAction = mergeSettingsLocal(projectDir);
|
|
150
|
+
ui.fileAction(hookAction, '.claude/settings.local.json');
|
|
151
|
+
} else {
|
|
152
|
+
ui.fileAction('skip', '.claude/settings.local.json (--no-hooks)');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// License key to .env.local
|
|
156
|
+
if (licenseKey) {
|
|
157
|
+
const keyAction = writeKeyToEnvLocal(licenseKey);
|
|
158
|
+
ui.fileAction(keyAction, '.env.local');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ────────────────────────────────────────
|
|
162
|
+
// Phase 5: Post-install checks
|
|
163
|
+
// ────────────────────────────────────────
|
|
164
|
+
console.log();
|
|
165
|
+
ui.step(5, TOTAL_STEPS, 'Post-install');
|
|
166
|
+
|
|
167
|
+
// Check .gitignore
|
|
168
|
+
if (isGitRepo) {
|
|
169
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
170
|
+
let gitignoreContent = '';
|
|
171
|
+
if (fs.existsSync(gitignorePath)) {
|
|
172
|
+
gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const missingEntries = [];
|
|
176
|
+
if (!gitignoreContent.includes('.env.local')) {
|
|
177
|
+
missingEntries.push('.env.local');
|
|
178
|
+
}
|
|
179
|
+
if (!gitignoreContent.includes('.orchestrix-core/runtime')) {
|
|
180
|
+
missingEntries.push('.orchestrix-core/runtime/');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (missingEntries.length > 0) {
|
|
184
|
+
ui.warn(`Add to .gitignore: ${missingEntries.join(', ')}`);
|
|
185
|
+
} else {
|
|
186
|
+
ui.success('.gitignore covers sensitive files');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Summary
|
|
191
|
+
ui.done();
|
|
192
|
+
|
|
193
|
+
ui.log('Next steps:');
|
|
194
|
+
ui.log('');
|
|
195
|
+
ui.log(' 1. Open this project in Claude Code');
|
|
196
|
+
ui.log(' 2. Type /o dev to activate the Developer agent');
|
|
197
|
+
ui.log(' 3. Type /o-help to see all available agents');
|
|
198
|
+
ui.log('');
|
|
199
|
+
if (!flags.noScripts) {
|
|
200
|
+
ui.log(' tmux automation (multi-agent):');
|
|
201
|
+
ui.log(' bash .orchestrix-core/scripts/start-orchestrix.sh');
|
|
202
|
+
ui.log('');
|
|
203
|
+
}
|
|
204
|
+
ui.log(` ${ui.colors.dim}For meta-orchestrator: npx orchestrix-yuri install${ui.colors.reset}`);
|
|
205
|
+
ui.log('');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Detect test command from project configuration
|
|
210
|
+
*/
|
|
211
|
+
function detectTestCommand(projectDir) {
|
|
212
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
213
|
+
if (fs.existsSync(pkgPath)) {
|
|
214
|
+
try {
|
|
215
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
216
|
+
if (pkg.scripts && pkg.scripts.test) {
|
|
217
|
+
return `npm run test`;
|
|
218
|
+
}
|
|
219
|
+
} catch { /* ignore */ }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Python
|
|
223
|
+
if (fs.existsSync(path.join(projectDir, 'pyproject.toml'))) {
|
|
224
|
+
return 'pytest';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Go
|
|
228
|
+
if (fs.existsSync(path.join(projectDir, 'go.mod'))) {
|
|
229
|
+
return 'go test ./...';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Rust
|
|
233
|
+
if (fs.existsSync(path.join(projectDir, 'Cargo.toml'))) {
|
|
234
|
+
return 'cargo test';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return 'npm run test';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = { install };
|
package/lib/license.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const ui = require('./ui');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolve license key from multiple sources (priority order):
|
|
9
|
+
* 1. --key CLI flag
|
|
10
|
+
* 2. ORCHESTRIX_LICENSE_KEY env var
|
|
11
|
+
* 3. .env.local file in cwd
|
|
12
|
+
* 4. Interactive prompt
|
|
13
|
+
*/
|
|
14
|
+
async function resolveKey(flags) {
|
|
15
|
+
// 1. CLI flag
|
|
16
|
+
if (flags.key) {
|
|
17
|
+
return flags.key;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 2. Environment variable
|
|
21
|
+
if (process.env.ORCHESTRIX_LICENSE_KEY) {
|
|
22
|
+
ui.info('Using license key from ORCHESTRIX_LICENSE_KEY env var');
|
|
23
|
+
return process.env.ORCHESTRIX_LICENSE_KEY;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 3. .env.local file
|
|
27
|
+
const envLocalPath = path.join(process.cwd(), '.env.local');
|
|
28
|
+
if (fs.existsSync(envLocalPath)) {
|
|
29
|
+
const content = fs.readFileSync(envLocalPath, 'utf-8');
|
|
30
|
+
const match = content.match(/^ORCHESTRIX_LICENSE_KEY=(.+)$/m);
|
|
31
|
+
if (match) {
|
|
32
|
+
ui.info('Using license key from .env.local');
|
|
33
|
+
return match[1].trim();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 4. Interactive prompt
|
|
38
|
+
const key = await ui.prompt('Enter your Orchestrix license key');
|
|
39
|
+
if (!key) {
|
|
40
|
+
throw new Error('License key is required. Use --key <KEY> or set ORCHESTRIX_LICENSE_KEY env var.');
|
|
41
|
+
}
|
|
42
|
+
return key;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Write license key to .env.local (append if file exists, create if not)
|
|
47
|
+
*/
|
|
48
|
+
function writeKeyToEnvLocal(key) {
|
|
49
|
+
const envLocalPath = path.join(process.cwd(), '.env.local');
|
|
50
|
+
let content = '';
|
|
51
|
+
|
|
52
|
+
if (fs.existsSync(envLocalPath)) {
|
|
53
|
+
content = fs.readFileSync(envLocalPath, 'utf-8');
|
|
54
|
+
|
|
55
|
+
// Check if key already exists
|
|
56
|
+
if (content.match(/^ORCHESTRIX_LICENSE_KEY=/m)) {
|
|
57
|
+
const existingMatch = content.match(/^ORCHESTRIX_LICENSE_KEY=(.+)$/m);
|
|
58
|
+
if (existingMatch && existingMatch[1].trim() === key) {
|
|
59
|
+
return 'skip'; // Same key, no change needed
|
|
60
|
+
}
|
|
61
|
+
ui.warn('ORCHESTRIX_LICENSE_KEY already exists in .env.local with a different value');
|
|
62
|
+
ui.warn('Please update it manually if needed');
|
|
63
|
+
return 'skip';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Append with newline separator
|
|
67
|
+
if (!content.endsWith('\n')) {
|
|
68
|
+
content += '\n';
|
|
69
|
+
}
|
|
70
|
+
content += `ORCHESTRIX_LICENSE_KEY=${key}\n`;
|
|
71
|
+
} else {
|
|
72
|
+
content = `ORCHESTRIX_LICENSE_KEY=${key}\n`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fs.writeFileSync(envLocalPath, content);
|
|
76
|
+
return fs.existsSync(envLocalPath) ? 'update' : 'create';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { resolveKey, writeKeyToEnvLocal };
|