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.
@@ -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 };