@_xtribe/cli 2.0.6 → 2.1.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.
Files changed (2) hide show
  1. package/install-tribe.js +139 -1934
  2. package/package.json +2 -2
package/install-tribe.js CHANGED
@@ -4,105 +4,86 @@ const os = require('os');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const https = require('https');
7
- const { execSync, spawn } = require('child_process');
8
- const readline = require('readline');
7
+ const { execSync } = require('child_process');
9
8
  const chalk = require('chalk');
10
9
  const ora = require('ora');
11
- const which = require('which');
12
- const fetch = require('node-fetch');
13
- const crypto = require('crypto'); // Added for checksum verification
10
+
11
+ // ASCII art for TRIBE
12
+ const asciiArt = `
13
+ ╔══════════════════════════════════════════════════════════════════╗
14
+ ║ ║
15
+ ║ ████████╗██████╗ ██╗██████╗ ███████╗ ║
16
+ ║ ╚══██╔══╝██╔══██╗██║██╔══██╗██╔════╝ ║
17
+ ║ ██║ ██████╔╝██║██████╔╝█████╗ ║
18
+ ║ ██║ ██╔══██╗██║██╔══██╗██╔══╝ ║
19
+ ║ ██║ ██║ ██║██║██████╔╝███████╗ ║
20
+ ║ ╚═╝ ╚═╝ ╚═╝╚═╝╚═════╝ ╚══════╝ ║
21
+ ║ ║
22
+ ║ Empower Your AI Agents • Become 10x Faster ║
23
+ ║ ║
24
+ ╚══════════════════════════════════════════════════════════════════╝`;
14
25
 
15
26
  const platform = os.platform();
16
- // Map Node.js arch to standard naming (x64 -> amd64, etc.)
17
- const nodeArch = os.arch();
18
- const arch = nodeArch === 'x64' ? 'amd64' : (nodeArch === 'arm64' || nodeArch === 'aarch64' ? 'arm64' : nodeArch);
27
+ const arch = os.arch() === 'x64' ? 'amd64' : (os.arch() === 'arm64' ? 'arm64' : os.arch());
19
28
  const homeDir = os.homedir();
20
29
  const tribeDir = path.join(homeDir, '.tribe');
21
30
  const tribeBinDir = path.join(tribeDir, 'bin');
22
- // Use consistent directory for all binaries
23
- const binDir = tribeBinDir;
24
-
25
- // No need to create ~/bin anymore since we're using ~/.tribe/bin
26
31
 
27
- // Ensure TRIBE config directory exists
32
+ // Ensure directories exist
28
33
  if (!fs.existsSync(tribeDir)) {
29
34
  fs.mkdirSync(tribeDir, { recursive: true });
30
35
  }
31
-
32
- // Ensure TRIBE bin directory exists
33
36
  if (!fs.existsSync(tribeBinDir)) {
34
37
  fs.mkdirSync(tribeBinDir, { recursive: true });
35
38
  }
36
39
 
37
- const log = {
38
- success: (msg) => console.log(chalk.green('✓'), msg),
39
- error: (msg) => console.log(chalk.red('✗'), msg),
40
- warning: (msg) => console.log(chalk.yellow(''), msg),
41
- info: (msg) => console.log(chalk.blue(''), msg),
42
- step: (msg) => console.log(chalk.cyan('→'), msg)
43
- };
44
-
45
- async function checkCommand(cmd) {
46
- try {
47
- await which(cmd);
48
- return true;
49
- } catch {
50
- // If not in PATH, check our install locations
51
- if (cmd === 'tribe') {
52
- const tribePath = path.join(tribeBinDir, 'tribe');
53
- try {
54
- await fs.promises.access(tribePath, fs.constants.X_OK);
55
- return true;
56
- } catch {
57
- return false;
58
- }
59
- } else if (cmd === 'kubectl') {
60
- const kubectlPath = path.join(binDir, 'kubectl');
61
- try {
62
- await fs.promises.access(kubectlPath, fs.constants.X_OK);
63
- return true;
64
- } catch {
65
- return false;
66
- }
67
- } else if (cmd === 'colima') {
68
- const colimaPath = path.join(binDir, 'colima');
69
- try {
70
- await fs.promises.access(colimaPath, fs.constants.X_OK);
71
- return true;
72
- } catch {
73
- return false;
74
- }
75
- }
76
- return false;
77
- }
40
+ function showWelcome() {
41
+ console.clear();
42
+ console.log(chalk.cyan(asciiArt));
43
+ console.log(chalk.bold('\n🚀 Welcome to TRIBE\n'));
44
+ console.log(chalk.bold('Track • Measure • Optimize Your AI Coding Agents\n'));
45
+
46
+ console.log('TRIBE empowers your AI development workflow by:');
47
+ console.log(' 📊 Tracking usage of Claude, Cursor, Copilot & more');
48
+ console.log(' 📈 Measuring productivity gains and patterns');
49
+ console.log(' ⚡ Optimizing how you work with AI agents');
50
+ console.log(' 🎯 Providing insights to make you 10x faster\n');
51
+
52
+ console.log(chalk.gray(`Platform: ${platform} (${arch})`));
53
+ console.log(chalk.gray(`Node: ${process.version}\n`));
78
54
  }
79
55
 
80
- async function findCommand(cmd) {
81
- // Try to find command in various locations
82
- const possiblePaths = [
83
- path.join(binDir, cmd), // Our install location
84
- path.join('/opt/homebrew/bin', cmd), // Homebrew on M1 Macs
85
- path.join('/usr/local/bin', cmd), // Homebrew on Intel Macs
86
- cmd // In PATH
87
- ];
56
+ function showInstallationSummary() {
57
+ console.log('\n' + '═'.repeat(70));
58
+ console.log(chalk.green.bold('\n✨ TRIBE Telemetry Ready!\n'));
88
59
 
89
- // First try 'which' command
90
- try {
91
- const cmdPath = await which(cmd);
92
- return cmdPath;
93
- } catch {
94
- // If not in PATH, check known locations
95
- for (const cmdPath of possiblePaths) {
96
- try {
97
- await fs.promises.access(cmdPath, fs.constants.X_OK);
98
- return cmdPath;
99
- } catch {
100
- // Continue searching
101
- }
102
- }
103
- }
60
+ console.log('📋 What We Installed:');
61
+ console.log(' ✓ TRIBE CLI for telemetry collection');
62
+ console.log(' ✓ Configuration in ~/.tribe directory');
63
+ console.log(' ✓ Automatic PATH setup\n');
64
+
65
+ console.log(chalk.bold('🎯 AI Agents We Track:'));
66
+ console.log(' 🤖 Claude (Anthropic)');
67
+ console.log(' ⚡ Cursor');
68
+ console.log(' 🐙 GitHub Copilot');
69
+ console.log(' 🧠 ChatGPT');
70
+ console.log(' 💻 Codeium');
71
+ console.log(' 🦾 And more...\n');
72
+
73
+ console.log(chalk.bold('🚀 Get Started in 3 Steps:'));
74
+ console.log(chalk.cyan(' 1. tribe login') + ' # Connect your account');
75
+ console.log(chalk.cyan(' 2. tribe enable') + ' # Start tracking');
76
+ console.log(chalk.cyan(' 3. tribe status') + ' # View your metrics\n');
104
77
 
105
- return null;
78
+ console.log(chalk.bold('💡 What Happens Next:'));
79
+ console.log(' • TRIBE automatically tracks your AI agent usage');
80
+ console.log(' • View insights at https://tribecode.ai/dashboard');
81
+ console.log(' • Get weekly reports on your productivity gains');
82
+ console.log(' • Join the community of 10x developers\n');
83
+
84
+ console.log(chalk.gray('Documentation: https://tribecode.ai/docs'));
85
+ console.log(chalk.gray('Support: hello@tribecode.ai'));
86
+ console.log('\n' + '═'.repeat(70) + '\n');
106
87
  }
107
88
 
108
89
  async function downloadFile(url, dest) {
@@ -110,1921 +91,145 @@ async function downloadFile(url, dest) {
110
91
  const file = fs.createWriteStream(dest);
111
92
  https.get(url, (response) => {
112
93
  if (response.statusCode === 302 || response.statusCode === 301) {
113
- // Handle redirects
114
94
  return downloadFile(response.headers.location, dest).then(resolve, reject);
115
95
  }
116
96
  if (response.statusCode !== 200) {
117
- reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
97
+ reject(new Error(`HTTP ${response.statusCode}`));
118
98
  return;
119
99
  }
120
100
  response.pipe(file);
121
- file.on('finish', async () => {
101
+ file.on('finish', () => {
122
102
  file.close();
123
-
124
- // Check if this might be a pointer file
125
- const stats = fs.statSync(dest);
126
- if (stats.size < 500) { // Increased size check - binaries are much larger
127
- const content = fs.readFileSync(dest, 'utf8').trim();
128
- // Check if content looks like a path (e.g., "main-121/tribe-linux-amd64")
129
- if (content.match(/^[\w\-\/]+$/) && !content.includes('<!DOCTYPE') && !content.includes('<html')) {
130
- console.log(`📎 Detected pointer file, following to: ${content}`);
131
- let actualBinaryUrl;
132
-
133
- // Handle different pointer formats
134
- if (content.startsWith('http')) {
135
- // Direct URL
136
- actualBinaryUrl = content;
137
- } else if (content.includes('/')) {
138
- // Relative path - construct full URL
139
- const baseUrl = url.substring(0, url.lastIndexOf('/'));
140
- actualBinaryUrl = `${baseUrl}/${content}`;
141
- } else {
142
- // Just a filename
143
- const baseUrl = url.substring(0, url.lastIndexOf('/'));
144
- actualBinaryUrl = `${baseUrl}/${content}`;
145
- }
146
-
147
- // Download the actual binary
148
- fs.unlinkSync(dest); // Remove pointer file
149
- await downloadFile(actualBinaryUrl, dest);
150
- }
151
- }
152
-
153
103
  resolve();
154
104
  });
155
105
  }).on('error', reject);
156
106
  });
157
107
  }
158
108
 
159
- // Docker installation removed - Colima provides Docker runtime
160
-
161
- async function installK3s() {
162
- const spinner = ora('Installing K3s (lightweight Kubernetes)...').start();
163
-
164
- try {
165
- // Check if K3s is already installed
166
- try {
167
- execSync('which k3s', { stdio: 'ignore' });
168
- spinner.succeed('K3s already installed');
169
- return true;
170
- } catch {
171
- // Not installed, continue
172
- }
173
-
174
- // Check if we have permission to install
175
- const needsSudo = process.getuid && process.getuid() !== 0;
176
-
177
- // Check if we're in a container or CI environment
178
- const isContainer = process.env.container === 'docker' ||
179
- fs.existsSync('/.dockerenv') ||
180
- !fs.existsSync('/run/systemd/system') ||
181
- process.env.CI === 'true';
182
-
183
- if (isContainer) {
184
- spinner.warn('Running in container/CI - K3s installation skipped');
185
- log.info('K3s requires systemd and privileged access');
186
- log.info('For containers, consider using an external cluster');
187
- return true; // Don't fail in containers
188
- }
189
-
190
- // Download and install K3s
191
- spinner.text = 'Downloading K3s installer...';
192
-
193
- const installCommand = needsSudo ?
194
- 'curl -sfL https://get.k3s.io | sudo sh -s - --write-kubeconfig-mode 644' :
195
- 'curl -sfL https://get.k3s.io | sh -s - --write-kubeconfig-mode 644';
196
-
197
- try {
198
- execSync(installCommand, {
199
- stdio: 'pipe',
200
- env: { ...process.env, INSTALL_K3S_SKIP_START: 'false' }
201
- });
202
-
203
- spinner.succeed('K3s installed successfully');
204
-
205
- // Wait for K3s to be ready
206
- spinner.text = 'Waiting for K3s to start...';
207
- await new Promise(resolve => setTimeout(resolve, 10000));
208
-
209
- // Configure kubectl to use K3s
210
- const k3sConfig = '/etc/rancher/k3s/k3s.yaml';
211
- const userConfig = path.join(os.homedir(), '.kube', 'config');
212
-
213
- if (fs.existsSync(k3sConfig)) {
214
- fs.mkdirSync(path.dirname(userConfig), { recursive: true });
215
-
216
- if (needsSudo) {
217
- execSync(`sudo cp ${k3sConfig} ${userConfig} && sudo chown $(id -u):$(id -g) ${userConfig}`, { stdio: 'ignore' });
218
- } else {
219
- fs.copyFileSync(k3sConfig, userConfig);
220
- }
221
-
222
- // Update server address to use localhost
223
- let configContent = fs.readFileSync(userConfig, 'utf8');
224
- configContent = configContent.replace(/server: https:\/\/127\.0\.0\.1:6443/, 'server: https://localhost:6443');
225
- fs.writeFileSync(userConfig, configContent);
226
-
227
- spinner.succeed('K3s configured');
228
- }
229
-
230
- return true;
231
- } catch (error) {
232
- if (error.message.includes('permission denied') || error.message.includes('sudo')) {
233
- spinner.fail('K3s installation requires sudo permission');
234
- log.info('Please run the installer with sudo or install K3s manually:');
235
- log.info(' curl -sfL https://get.k3s.io | sudo sh -');
236
- return false;
237
- }
238
- throw error;
239
- }
240
-
241
- } catch (error) {
242
- spinner.fail('K3s installation failed');
243
- log.error(error.message);
244
- log.info('Manual K3s installation:');
245
- log.info(' curl -sfL https://get.k3s.io | sudo sh -');
246
- log.info(' sudo systemctl enable --now k3s');
247
- return false;
248
- }
249
- }
250
-
251
- async function installColima() {
252
- if (platform !== 'darwin') {
253
- log.info('Colima is only needed on macOS - skipping');
254
- return true;
255
- }
256
-
257
- const existingColima = await findCommand('colima');
258
- if (existingColima) {
259
- log.success(`Colima already installed at: ${existingColima}`);
260
- return true;
261
- }
262
-
263
- const spinner = ora('Installing Colima...').start();
264
-
265
- try {
266
- // Strategy 1: Try Homebrew if available (better signing)
267
- try {
268
- execSync('brew --version', { stdio: 'ignore' });
269
- spinner.text = 'Installing Colima via Homebrew...';
270
- execSync('brew install colima', { stdio: 'ignore' });
271
- spinner.succeed('Colima installed via Homebrew');
272
- return true;
273
- } catch (brewError) {
274
- // Homebrew not available, continue with direct download
275
- spinner.text = 'Installing Colima (direct download)...';
276
- }
277
-
278
- // Strategy 2: Direct download with Gatekeeper approval
279
- const colimaUrl = `https://github.com/abiosoft/colima/releases/latest/download/colima-${platform}-${arch}`;
280
- const colimaDest = path.join(binDir, 'colima');
281
-
282
- await downloadFile(colimaUrl, colimaDest);
283
- fs.chmodSync(colimaDest, '755');
284
-
285
- // Try to remove quarantine attribute (macOS Sequoia workaround)
286
- try {
287
- execSync(`xattr -d com.apple.quarantine ${colimaDest}`, { stdio: 'ignore' });
288
- log.info('Removed quarantine attribute from Colima');
289
- } catch (error) {
290
- // Quarantine attribute may not exist, which is fine
291
- log.info('Colima installed (quarantine handling not needed)');
292
- }
293
-
294
- spinner.succeed('Colima installed');
295
- return true;
296
- } catch (error) {
297
- spinner.fail(`Colima installation failed: ${error.message}`);
298
- return false;
299
- }
300
- }
301
-
302
- // Lima installation removed - Colima handles virtualization
303
-
304
-
305
- async function installKubectl() {
306
- // Linux with K3s already has kubectl
307
- if (platform === 'linux') {
308
- try {
309
- execSync('k3s kubectl version --client', { stdio: 'ignore' });
310
- log.success('kubectl available via k3s');
311
- // Create a symlink for convenience
312
- try {
313
- execSync('sudo ln -sf /usr/local/bin/k3s /usr/local/bin/kubectl', { stdio: 'ignore' });
314
- } catch {
315
- // Link might already exist
316
- }
317
- return true;
318
- } catch {
319
- // Continue with regular kubectl installation
320
- }
321
- }
322
-
323
- if (await checkCommand('kubectl')) {
324
- log.success('kubectl already installed');
325
- return true;
326
- }
327
-
328
- const spinner = ora('Installing kubectl...').start();
329
-
330
- try {
331
- // Get latest stable version
332
- const versionResponse = await fetch('https://dl.k8s.io/release/stable.txt');
333
- const version = await versionResponse.text();
334
- const kubectlUrl = `https://dl.k8s.io/release/${version.trim()}/bin/${platform}/${arch}/kubectl`;
335
- const kubectlDest = path.join(binDir, 'kubectl');
336
-
337
- await downloadFile(kubectlUrl, kubectlDest);
338
- fs.chmodSync(kubectlDest, '755');
339
-
340
- spinner.succeed('kubectl installed');
341
- return true;
342
- } catch (error) {
343
- spinner.fail(`kubectl installation failed: ${error.message}`);
344
- return false;
345
- }
346
- }
347
-
348
- async function setupPathEnvironment() {
349
- const spinner = ora('Setting up PATH environment...').start();
350
-
351
- try {
352
- // Copy the tribe-env.sh script to ~/.tribe/
353
- const envScriptSource = path.join(__dirname, 'tribe-env.sh');
354
- const envScriptDest = path.join(tribeBinDir, '..', 'tribe-env.sh');
355
-
356
- // Create the environment script content
357
- const envScriptContent = `#!/bin/bash
358
- # TRIBE CLI Environment Setup
359
- #
360
- # To use this, add the following line to your shell config:
361
- # source ~/.tribe/tribe-env.sh
362
- #
363
- # Or run it manually in your current session:
364
- # source ~/.tribe/tribe-env.sh
365
-
366
- # Add TRIBE bin directory to PATH if not already present
367
- TRIBE_BIN_DIR="$HOME/.tribe/bin"
368
-
369
- if [[ -d "$TRIBE_BIN_DIR" ]] && [[ ":$PATH:" != *":$TRIBE_BIN_DIR:"* ]]; then
370
- export PATH="$TRIBE_BIN_DIR:$PATH"
371
- fi
372
-
373
- # Optional: Add helpful aliases
374
- alias tribe-logs="$TRIBE_BIN_DIR/tribe-logs" 2>/dev/null || true
375
-
376
- # Verify tribe is available (silent check)
377
- command -v tribe &> /dev/null || true
378
- `;
379
-
380
- fs.writeFileSync(envScriptDest, envScriptContent);
381
- fs.chmodSync(envScriptDest, '755');
382
-
383
- spinner.succeed('PATH environment script created');
384
-
385
- // Store that we created the env script
386
- global.envScriptCreated = true;
387
-
388
- // Now automatically run setup-path to configure the shell
389
- try {
390
- const setupPath = require('./setup-path.js');
391
- setupPath();
392
- global.pathSetupComplete = true;
393
- } catch (error) {
394
- // If setup-path fails, we'll still show manual instructions
395
- log.info('Could not automatically configure PATH');
396
- }
397
-
398
- return true;
399
- } catch (error) {
400
- spinner.fail(`Failed to create PATH environment script: ${error.message}`);
401
- return false;
402
- }
403
- }
404
-
405
- async function setupGlobalNpmCommand() {
406
- const spinner = ora('Setting up global tribe command...').start();
407
-
408
- try {
409
- // Create a temporary directory for the global package
410
- const tempDir = path.join(os.tmpdir(), 'tribe-global-install-' + Date.now());
411
- fs.mkdirSync(tempDir, { recursive: true });
412
-
413
- // Create package.json for global command
414
- const packageJson = {
415
- name: 'tribe-cli-global',
416
- version: '1.0.0',
417
- description: 'TRIBE CLI global command',
418
- bin: {
419
- tribe: './tribe-wrapper.js',
420
- 'tribe-logs': './tribe-logs-wrapper.js'
421
- },
422
- files: ['tribe-wrapper.js', 'tribe-logs-wrapper.js'],
423
- private: true
424
- };
425
-
426
- fs.writeFileSync(
427
- path.join(tempDir, 'package.json'),
428
- JSON.stringify(packageJson, null, 2)
429
- );
430
-
431
- // Create the log viewer wrapper script
432
- const logViewerScript = `#!/usr/bin/env node
433
-
434
- const { spawn } = require('child_process');
435
- const fs = require('fs');
436
- const path = require('path');
437
- const os = require('os');
438
-
439
- // Path to the actual tribe-logs script
440
- const logsScriptPath = path.join(os.homedir(), '.tribe', 'bin', 'tribe-logs');
441
-
442
- // Check if script exists
443
- if (!fs.existsSync(logsScriptPath)) {
444
- console.error('Error: tribe-logs script not found.');
445
- console.error('Please reinstall using: npx @_xtribe/cli --force');
446
- process.exit(1);
447
- }
448
-
449
- // Forward all arguments
450
- const child = spawn(logsScriptPath, process.argv.slice(2), {
451
- stdio: 'inherit',
452
- env: process.env
453
- });
454
-
455
- child.on('exit', (code) => {
456
- process.exit(code || 0);
457
- });
458
-
459
- child.on('error', (err) => {
460
- console.error('Failed to execute tribe-logs:', err.message);
461
- process.exit(1);
462
- });`;
463
-
464
- const logViewerPath = path.join(tempDir, 'tribe-logs-wrapper.js');
465
- fs.writeFileSync(logViewerPath, logViewerScript);
466
- fs.chmodSync(logViewerPath, '755');
467
-
468
- // Create the wrapper script
469
- const wrapperScript = `#!/usr/bin/env node
470
-
471
- const { spawn } = require('child_process');
472
- const fs = require('fs');
473
- const path = require('path');
474
- const os = require('os');
475
-
476
- // Path to the actual tribe binary installed by the installer
477
- const tribeBinaryPath = path.join(os.homedir(), '.tribe', 'bin', 'tribe');
478
-
479
- // Check if tribe binary exists
480
- if (!fs.existsSync(tribeBinaryPath)) {
481
- console.error('Error: TRIBE CLI binary not found.');
482
- console.error('Please reinstall using: npx @_xtribe/cli --force');
483
- process.exit(1);
484
- }
485
-
486
- // Make sure binary is executable
487
- try {
488
- fs.accessSync(tribeBinaryPath, fs.constants.X_OK);
489
- } catch (err) {
490
- console.error('Error: TRIBE CLI binary is not executable.');
491
- console.error('Please reinstall using: npx @_xtribe/cli --force');
492
- process.exit(1);
493
- }
494
-
495
- // Forward all arguments to the actual tribe binary
496
- const child = spawn(tribeBinaryPath, process.argv.slice(2), {
497
- stdio: 'inherit',
498
- env: process.env
499
- });
500
-
501
- child.on('exit', (code) => {
502
- process.exit(code || 0);
503
- });
504
-
505
- child.on('error', (err) => {
506
- if (err.code === 'ENOENT') {
507
- console.error('Error: TRIBE CLI binary not found at expected location.');
508
- console.error('Please reinstall using: npx @_xtribe/cli --force');
509
- } else {
510
- console.error('Failed to execute tribe:', err.message);
511
- }
512
- process.exit(1);
513
- });`;
514
-
515
- const wrapperPath = path.join(tempDir, 'tribe-wrapper.js');
516
- fs.writeFileSync(wrapperPath, wrapperScript);
517
- fs.chmodSync(wrapperPath, '755');
518
-
519
- // Install globally using npm
520
- spinner.text = 'Installing tribe command globally...';
521
- try {
522
- // First try with --force to handle conflicts
523
- execSync('npm install -g . --force', {
524
- cwd: tempDir,
525
- stdio: 'pipe'
526
- });
527
- spinner.succeed('Global tribe command installed successfully');
528
- } catch (error) {
529
- // Try with sudo if permission denied
530
- if (error.message.includes('EACCES') || error.message.includes('permission')) {
531
- spinner.warn('Global installation requires sudo permission');
532
- log.info('Attempting with sudo...');
533
- try {
534
- execSync('sudo npm install -g . --force', {
535
- cwd: tempDir,
536
- stdio: 'inherit'
537
- });
538
- spinner.succeed('Global tribe command installed successfully (with sudo)');
539
- } catch (sudoError) {
540
- throw new Error('Failed to install globally. You may need to run with sudo or fix npm permissions.');
541
- }
542
- } else if (error.message.includes('EEXIST')) {
543
- // Try to remove existing and retry
544
- spinner.text = 'Removing conflicting global command...';
545
- try {
546
- execSync('npm uninstall -g tribe-cli-global @_xtribe/cli', { stdio: 'ignore' });
547
- execSync('npm install -g . --force', {
548
- cwd: tempDir,
549
- stdio: 'pipe'
550
- });
551
- spinner.succeed('Global tribe command installed successfully (after cleanup)');
552
- } catch (retryError) {
553
- throw new Error('Failed to install globally due to conflicts. Try running: npm uninstall -g @_xtribe/cli');
554
- }
555
- } else {
556
- throw error;
557
- }
558
- }
559
-
560
- // Clean up temp directory
561
- try {
562
- fs.rmSync(tempDir, { recursive: true, force: true });
563
- } catch (cleanupError) {
564
- // Ignore cleanup errors
565
- }
566
-
567
- return true;
568
- } catch (error) {
569
- spinner.fail(`Failed to set up global command: ${error.message}`);
570
- log.info('You can still use tribe via the direct path: ~/.tribe/bin/tribe');
571
- return false;
572
- }
573
- }
574
-
575
- async function installTribeLogViewer() {
576
- try {
577
- // Install the log viewer script
578
- const logViewerSource = path.join(__dirname, 'tribe-logs.sh');
579
- const logViewerDest = path.join(tribeBinDir, 'tribe-logs');
580
-
581
- if (fs.existsSync(logViewerSource)) {
582
- fs.copyFileSync(logViewerSource, logViewerDest);
583
- fs.chmodSync(logViewerDest, '755');
584
- log.success('Installed tribe-logs command for viewing debug logs');
585
- }
586
- } catch (error) {
587
- log.warning(`Could not install log viewer: ${error.message}`);
588
- }
589
- }
590
-
591
109
  async function installTribeCLI() {
592
- const spinner = ora('Installing TRIBE CLI...').start();
110
+ const tribeDest = path.join(tribeBinDir, 'tribe');
111
+
112
+ // Check if update needed
113
+ const exists = fs.existsSync(tribeDest);
114
+ const spinner = ora(exists ? '🔄 Updating TRIBE CLI...' : '📥 Installing TRIBE CLI...').start();
593
115
 
594
116
  try {
595
- const tribeDest = path.join(tribeBinDir, 'tribe');
596
-
597
- // Unconditionally remove existing binary to ensure a clean install
598
- if (fs.existsSync(tribeDest)) {
599
- spinner.text = 'Removing existing TRIBE CLI installation...';
117
+ // Remove existing if present
118
+ if (exists) {
600
119
  fs.unlinkSync(tribeDest);
601
- log.success('Removed existing TRIBE CLI');
602
- spinner.text = 'Installing TRIBE CLI...'; // Reset spinner text
603
120
  }
604
121
 
605
- // Also remove old location if exists
606
- const oldTribeDest = path.join(binDir, 'tribe');
607
- if (fs.existsSync(oldTribeDest)) {
608
- try {
609
- fs.unlinkSync(oldTribeDest);
610
- log.info('Removed old tribe binary from ~/bin');
611
- } catch (e) {
612
- // Ignore errors
613
- }
614
- }
615
-
616
- // Check for local binary first (for development/testing)
617
- const localBinary = path.join(__dirname, 'tribe');
618
- if (fs.existsSync(localBinary)) {
619
- spinner.text = 'Using local TRIBE binary...';
620
- fs.copyFileSync(localBinary, tribeDest);
621
- fs.chmodSync(tribeDest, '755');
622
- spinner.succeed('Installed TRIBE CLI from local binary');
623
- return;
624
- }
625
-
626
- // Download pre-built binary from GitHub
627
- // Proper architecture detection for all platforms
628
- let arch;
629
- if (process.arch === 'x64' || process.arch === 'x86_64') {
630
- arch = 'amd64';
631
- } else if (process.arch === 'arm64' || process.arch === 'aarch64') {
632
- arch = 'arm64';
633
- } else {
634
- arch = process.arch; // fallback
635
- }
636
-
637
- const platform = os.platform();
638
-
639
- // Multiple sources for reliability
640
- // For testing, use a direct URL to our releases folder
641
- const isTestEnv = process.env.TRIBE_TEST_BINARY_URL;
642
- const testBinary = process.env.TRIBE_TEST_BINARY;
643
- const githubRepo = process.env.TRIBE_INSTALLER_REPO || 'TRIBE-INC/releases';
644
-
645
- // If we have a test binary file, use it directly
646
- if (testBinary && fs.existsSync(testBinary)) {
647
- spinner.text = 'Using test binary...';
648
- fs.copyFileSync(testBinary, tribeDest);
649
- fs.chmodSync(tribeDest, '755');
650
- spinner.succeed('✓ TRIBE CLI installed from test binary');
651
- return true;
652
- }
653
-
654
- let downloadUrl = '';
655
-
656
- try {
657
- const versionChannel = global.versionChannel || 'latest';
658
- spinner.text = `Fetching ${versionChannel} release information from GitHub...`;
659
- const response = await fetch(`https://api.github.com/repos/${githubRepo}/releases`, {
660
- headers: {
661
- 'User-Agent': 'TRIBE-Installer',
662
- // Add token if available in environment
663
- ...(process.env.GITHUB_TOKEN ? { 'Authorization': `token ${process.env.GITHUB_TOKEN}` } : {})
664
- }
665
- });
666
- if (!response.ok) {
667
- throw new Error(`GitHub API returned ${response.status}`);
668
- }
669
- const releases = await response.json();
670
- if (!releases || releases.length === 0) {
671
- throw new Error('No releases found');
672
- }
673
-
674
- // Filter releases based on version channel
675
- let filteredReleases = releases.filter(r => !r.draft);
676
-
677
- if (versionChannel === 'beta') {
678
- // For beta, look for releases with 'beta' in tag name
679
- filteredReleases = filteredReleases.filter(r => r.tag_name.includes('beta'));
680
- spinner.text = 'Looking for beta releases...';
681
- } else if (versionChannel === 'latest') {
682
- // For latest, exclude beta releases
683
- filteredReleases = filteredReleases.filter(r => !r.tag_name.includes('beta'));
684
- spinner.text = 'Looking for stable releases...';
685
- } else {
686
- // For specific version, look for exact match
687
- filteredReleases = filteredReleases.filter(r => r.tag_name === versionChannel);
688
- spinner.text = `Looking for version ${versionChannel}...`;
689
- }
690
-
691
- if (filteredReleases.length === 0) {
692
- throw new Error(`No releases found for channel: ${versionChannel}`);
693
- }
694
-
695
- // Find a release with the binary we need
696
- let foundRelease = null;
697
- const assetName = `tribe-${platform}-${arch}`;
698
-
699
- for (const release of filteredReleases) {
700
- const asset = release.assets?.find(a => a.name === assetName || a.name === `${assetName}.exe`);
701
- if (asset) {
702
- foundRelease = release;
703
- downloadUrl = asset.browser_download_url;
704
- spinner.text = `Found binary in ${versionChannel} release: ${release.tag_name}`;
705
- break;
706
- }
707
- }
708
-
709
- if (!foundRelease) {
710
- // No release has the binary, try constructing URL from first filtered release
711
- const firstRelease = filteredReleases[0];
712
- if (firstRelease) {
713
- downloadUrl = `https://github.com/${githubRepo}/releases/download/${firstRelease.tag_name}/tribe-${platform}-${arch}`;
714
- spinner.text = `Trying ${versionChannel} release: ${firstRelease.tag_name}`;
715
- } else {
716
- throw new Error(`No suitable release found for channel: ${versionChannel}`);
717
- }
718
- }
719
- } catch (error) {
720
- log.warning(`Failed to get release info: ${error.message}`);
721
- // Try to get the latest release tag directly
722
- try {
723
- const tagResponse = await fetch(`https://api.github.com/repos/${githubRepo}/releases`, {
724
- headers: { 'User-Agent': 'TRIBE-Installer' }
725
- });
726
- if (tagResponse.ok) {
727
- const releases = await tagResponse.json();
728
- if (releases && releases.length > 0) {
729
- const tag = releases[0].tag_name;
730
- // Construct direct download URL
731
- downloadUrl = `https://github.com/${githubRepo}/releases/download/${tag}/tribe-${platform}-${arch}`;
732
- spinner.text = `Using direct download for release: ${tag}`;
733
- }
734
- }
735
- } catch (e) {
736
- // Ignore and use fallback URLs
737
- }
738
- }
739
-
740
- // Check if there's a local build available (use tribe-new, not cli-gui)
741
- const localBuildPath = path.join(__dirname, '..', '..', '..', 'sdk', 'cmd', 'tribe-new', 'tribe');
742
- const hasLocalBuild = fs.existsSync(localBuildPath);
743
-
744
- const sources = isTestEnv ? [isTestEnv] : [
745
- downloadUrl,
746
- // GitHub releases direct download (fallback if API fails)
747
- `https://github.com/${githubRepo}/releases/latest/download/tribe-${platform}-${arch}`,
748
- // GitHub raw content as emergency fallback
749
- `https://raw.githubusercontent.com/${githubRepo}/main/releases/tribe-${platform}-${arch}`,
750
- // jsdelivr CDN (mirrors GitHub)
751
- `https://cdn.jsdelivr.net/gh/${githubRepo}@latest/releases/tribe-${platform}-${arch}`,
752
- // Local build if available
753
- ...(hasLocalBuild ? [`file://${localBuildPath}`] : [])
754
- ].filter(Boolean); // Filter out empty downloadUrl if API failed
122
+ // Download from GitHub
123
+ const githubRepo = 'TRIBE-INC/releases';
124
+ const downloadUrl = `https://github.com/${githubRepo}/releases/latest/download/tribe-${platform}-${arch}`;
755
125
 
756
- let downloaded = false;
757
- let lastError;
126
+ await downloadFile(downloadUrl, tribeDest);
127
+ fs.chmodSync(tribeDest, '755');
758
128
 
759
- for (const url of sources) {
760
- try {
761
- let downloadUrl = url;
762
-
763
- // Check if this is the GitHub API endpoint
764
- if (url.includes('/api.github.com/')) {
765
- spinner.text = 'Fetching latest release information from GitHub...';
766
-
767
- // Fetch release data
768
- const response = await fetch(url);
769
- if (!response.ok) {
770
- throw new Error(`GitHub API returned ${response.status}`);
771
- }
772
-
773
- const releaseData = await response.json();
774
-
775
- // Handle both single release and array of releases
776
- const release = Array.isArray(releaseData) ? releaseData[0] : releaseData;
777
-
778
- if (!release || !release.assets) {
779
- throw new Error('No release found');
780
- }
781
-
782
- const assetName = `tribe-${platform}-${arch}`;
783
- const asset = release.assets.find(a => a.name === assetName);
784
-
785
- if (!asset) {
786
- throw new Error(`No binary found for ${platform}-${arch}`);
787
- }
788
-
789
- downloadUrl = asset.browser_download_url;
790
- spinner.text = `Found ${release.prerelease ? 'pre-release' : 'release'}: ${release.tag_name}`;
791
- }
792
-
793
- if (downloadUrl.startsWith('file://')) {
794
- // Handle local file copy
795
- const sourcePath = downloadUrl.replace('file://', '');
796
- spinner.text = `Copying TRIBE CLI from local build...`;
797
- fs.copyFileSync(sourcePath, tribeDest);
798
- } else {
799
- spinner.text = `Downloading TRIBE CLI from ${new URL(downloadUrl).hostname}...`;
800
- await downloadFile(downloadUrl, tribeDest);
801
- }
802
-
803
- // Check if file is not empty
804
- const stats = fs.statSync(tribeDest);
805
- if (stats.size === 0) {
806
- throw new Error('Downloaded file is empty');
807
- }
808
-
809
- fs.chmodSync(tribeDest, '755');
810
-
811
- // Remove macOS quarantine attribute
812
- if (platform === 'darwin') {
813
- try {
814
- execSync(`xattr -d com.apple.quarantine "${tribeDest}" 2>/dev/null`, { stdio: 'ignore' });
815
- log.info('Removed macOS quarantine attribute');
816
- } catch {
817
- // Not critical if this fails
818
- }
819
- }
820
-
821
- // Verify the binary works
822
- try {
823
- // Try version command first
824
- execSync(`${tribeDest} version`, { stdio: 'ignore' });
825
- } catch (versionError) {
826
- // If version command doesn't exist, try help to see if binary is valid
827
- try {
828
- const helpOutput = execSync(`${tribeDest} help`, { encoding: 'utf8' });
829
- if (helpOutput.includes('TRIBE') || helpOutput.includes('tribe')) {
830
- // Binary works, just doesn't have version command
831
- log.info('Binary validated (version command not available)');
832
- } else {
833
- throw new Error('Binary validation failed');
834
- }
835
- } catch (helpError) {
836
- // Binary might not be valid
837
- log.warning('Binary downloaded but validation failed - may need different architecture');
838
- }
839
- }
840
-
841
- spinner.succeed('TRIBE CLI installed successfully');
842
-
843
- // Install the log viewer
844
- await installTribeLogViewer();
845
-
846
- downloaded = true;
847
- break;
848
- } catch (error) {
849
- lastError = error;
850
- // Clean up failed download
851
- if (fs.existsSync(tribeDest)) {
852
- fs.unlinkSync(tribeDest);
853
- }
854
- log.warning(`Failed: ${error.message}. Trying next source...`);
855
- }
856
- }
857
-
858
- if (!downloaded) {
859
- throw new Error(`Failed to download TRIBE CLI. Please check your internet connection and try again.
860
-
861
- If this persists, the binaries may not be available for your platform (${platform}-${arch}).
862
- Please visit https://tribecode.ai/support for assistance.`);
863
- }
864
-
865
- return true;
866
- } catch (error) {
867
- spinner.fail(`TRIBE CLI installation failed: ${error.message}`);
868
-
869
- // Show helpful support information
870
- console.log('');
871
- console.log(chalk.yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
872
- console.log(chalk.yellow('Need help? Visit:'));
873
- console.log(chalk.cyan.bold(' https://tribecode.ai/support'));
874
- console.log('');
875
- console.log(chalk.gray('You can also:'));
876
- console.log(chalk.gray(' • Check system requirements'));
877
- console.log(chalk.gray(' • View troubleshooting guides'));
878
- console.log(chalk.gray(' • Contact our support team'));
879
- console.log(chalk.yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
880
- console.log('');
881
-
882
- return false;
883
- }
884
- }
885
-
886
- async function startContainerRuntime() {
887
- const spinner = ora('Starting container runtime...').start();
888
-
889
- try {
890
- // Platform-specific container runtime handling
129
+ // Remove macOS quarantine if needed
891
130
  if (platform === 'darwin') {
892
- // macOS - Check if Colima is running
893
- try {
894
- const colimaStatus = execSync('colima status 2>&1', { encoding: 'utf8' });
895
- if (colimaStatus.includes('is running')) {
896
- spinner.succeed('Colima is already running');
897
- return true;
898
- }
899
- } catch {
900
- // Colima not running, need to start it
901
- }
902
-
903
- // Start Colima automatically
904
- if (await checkCommand('colima')) {
905
- spinner.text = 'Starting Colima with Kubernetes (this may take a few minutes on first run)...';
906
-
907
- // Start Colima in the background
908
- const { spawn } = require('child_process');
909
- const colimaProcess = spawn('colima', ['start', '--kubernetes'], {
910
- stdio: ['ignore', 'pipe', 'pipe'],
911
- detached: false
912
- });
913
-
914
- let output = '';
915
- let errorOutput = '';
916
- let startupComplete = false;
917
-
918
- // Monitor output
919
- colimaProcess.stdout.on('data', (data) => {
920
- output += data.toString();
921
- // Update spinner with progress
922
- const lines = data.toString().split('\n').filter(line => line.trim());
923
- for (const line of lines) {
924
- if (line.includes('starting ...')) {
925
- const contextMatch = line.match(/context=(\w+)/);
926
- if (contextMatch) {
927
- spinner.text = `Starting Colima: ${contextMatch[1]}...`;
928
- }
929
- } else if (line.includes('provisioning ...')) {
930
- const contextMatch = line.match(/context=(\w+)/);
931
- if (contextMatch) {
932
- spinner.text = `Provisioning ${contextMatch[1]}...`;
933
- }
934
- } else if (line.includes('done')) {
935
- startupComplete = true;
936
- }
937
- }
938
- });
939
-
940
- colimaProcess.stderr.on('data', (data) => {
941
- errorOutput += data.toString();
942
- // Colima outputs progress to stderr, not stdout
943
- const lines = data.toString().split('\n').filter(line => line.trim());
944
- for (const line of lines) {
945
- if (line.includes('starting ...')) {
946
- const contextMatch = line.match(/context=(\w+)/);
947
- if (contextMatch) {
948
- spinner.text = `Starting Colima: ${contextMatch[1]}...`;
949
- }
950
- } else if (line.includes('provisioning ...')) {
951
- const contextMatch = line.match(/context=(\w+)/);
952
- if (contextMatch) {
953
- spinner.text = `Provisioning ${contextMatch[1]}...`;
954
- }
955
- } else if (line.includes('done')) {
956
- startupComplete = true;
957
- }
958
- }
959
- });
960
-
961
- // Wait for process to complete
962
- const exitCode = await new Promise((resolve) => {
963
- colimaProcess.on('exit', (code) => {
964
- resolve(code);
965
- });
966
- });
967
-
968
- if (exitCode === 0 && startupComplete) {
969
- // Verify Colima is actually running
970
- try {
971
- execSync('colima status', { stdio: 'ignore' });
972
- spinner.succeed('Colima started with Kubernetes');
973
-
974
- // Give k3s a moment to initialize
975
- spinner.text = 'Waiting for Kubernetes to initialize...';
976
- await new Promise(resolve => setTimeout(resolve, 5000));
977
-
978
- return true;
979
- } catch {
980
- spinner.fail('Colima started but is not responding');
981
- return false;
982
- }
983
- } else {
984
- spinner.fail('Failed to start Colima');
985
- // Only show actual error messages, not progress
986
- const actualErrors = errorOutput.split('\n')
987
- .filter(line => line.trim() &&
988
- !line.includes('level=info') &&
989
- !line.includes('starting ...') &&
990
- !line.includes('provisioning ...') &&
991
- !line.includes('done'))
992
- .join('\n');
993
-
994
- if (actualErrors.trim()) {
995
- console.log(chalk.red('Error output:'));
996
- console.log(actualErrors);
997
- }
998
- console.log('');
999
- console.log(chalk.yellow(' Please start Colima manually:'));
1000
- console.log(chalk.cyan(' colima start --kubernetes'));
1001
- console.log('');
1002
- return false;
1003
- }
1004
- } else {
1005
- spinner.warn('Colima not found');
1006
- return false;
1007
- }
1008
- } else if (platform === 'linux') {
1009
- // Linux - Check K3s status but don't try to start it
1010
- spinner.text = 'Checking K3s status...';
1011
-
1012
- // Check if we're in a container (no systemd)
1013
- const isContainer = process.env.container === 'docker' || fs.existsSync('/.dockerenv') || !fs.existsSync('/run/systemd/system');
1014
-
1015
- if (isContainer) {
1016
- spinner.info('Running in container - K3s requires systemd');
1017
- log.info('For containers, connect to an external cluster');
1018
- return true; // Don't fail
1019
- }
1020
-
1021
- try {
1022
- execSync('sudo systemctl is-active --quiet k3s', { stdio: 'ignore', timeout: 5000 });
1023
- spinner.succeed('K3s is running');
1024
- return true;
1025
- } catch {
1026
- // Don't try to start K3s - just inform the user
1027
- spinner.info('K3s is not running');
1028
- console.log('');
1029
- console.log(chalk.yellow(' ⚠️ K3s needs to be started'));
1030
- console.log(chalk.gray(' To start K3s:'));
1031
- console.log(chalk.cyan(' sudo systemctl start k3s'));
1032
- console.log('');
1033
- return true; // Don't fail
1034
- }
1035
- } else if (platform === 'win32') {
1036
- spinner.fail('Windows support coming soon!');
1037
- log.info('For Windows, please use WSL2 with Ubuntu and run this installer inside WSL2');
1038
- return false;
1039
- } else {
1040
- spinner.fail(`Unsupported platform: ${platform}`);
1041
- return false;
1042
- }
1043
- } catch (error) {
1044
- spinner.fail(`Container runtime error: ${error.message}`);
1045
- return false;
1046
- }
1047
- }
1048
-
1049
- // PATH updates no longer needed - using npm global installation
1050
- // async function updatePath() { ... }
1051
-
1052
- // Shell config updates no longer needed - using npm global installation
1053
- // async function updateShellConfig() { ... }
1054
-
1055
- async function verifyInstallation() {
1056
- const spinner = ora('Verifying installation...').start();
1057
-
1058
- const tools = ['kubectl', 'tribe', 'colima'];
1059
- const results = {};
1060
-
1061
- for (const tool of tools) {
1062
- results[tool] = await checkCommand(tool);
1063
- }
1064
-
1065
- // Store results globally for later use
1066
- global.installationChecks = results;
1067
-
1068
- // Special check for container runtime
1069
- let containerWorking = false;
1070
- if (platform === 'darwin') {
1071
- try {
1072
- const colimaStatus = execSync('colima status 2>&1', { encoding: 'utf8' });
1073
- containerWorking = colimaStatus.includes('is running');
1074
- } catch {
1075
- containerWorking = false;
1076
- }
1077
- } else if (platform === 'linux') {
1078
- try {
1079
- // Check if k3s is running
1080
- execSync('systemctl is-active k3s', { stdio: 'ignore' });
1081
- containerWorking = true;
1082
- } catch {
1083
- // Fallback to kubectl
1084
- try {
1085
- execSync('kubectl cluster-info', { stdio: 'ignore' });
1086
- containerWorking = true;
1087
- } catch {
1088
- containerWorking = false;
1089
- }
1090
- }
1091
- }
1092
-
1093
- spinner.stop();
1094
-
1095
- console.log('\n' + chalk.bold('Installation Summary:'));
1096
- for (const tool of tools) {
1097
- const status = results[tool] ? chalk.green('✓') : chalk.red('✗');
1098
- let message = `${status} ${tool}`;
1099
-
1100
- // Add additional info for tribe if it's installed but not in PATH
1101
- if (tool === 'tribe' && results[tool]) {
1102
131
  try {
1103
- await which('tribe');
1104
- // It's in PATH, no extra message needed
132
+ execSync(`xattr -d com.apple.quarantine "${tribeDest}"`, { stdio: 'ignore' });
1105
133
  } catch {
1106
- // It's installed but not in PATH
1107
- message += chalk.gray(' (installed at ~/.tribe/bin/tribe)');
1108
- if (global.envScriptCreated) {
1109
- message += chalk.yellow('\n Run: source ~/.tribe/tribe-env.sh');
1110
- }
1111
- }
1112
- }
1113
-
1114
- console.log(message);
1115
- }
1116
-
1117
- const extraStatus = containerWorking ? chalk.green('✓') : chalk.yellow('⚠');
1118
- console.log(`${extraStatus} Container runtime`);
1119
-
1120
- if (platform === 'darwin') {
1121
- const colimaInstalled = await checkCommand('colima');
1122
- const limaInstalled = await checkCommand('limactl');
1123
- console.log(`${colimaInstalled ? chalk.green('✓') : chalk.yellow('⚠')} Colima`);
1124
- console.log(`${limaInstalled ? chalk.green('✓') : chalk.yellow('⚠')} Lima`);
1125
- }
1126
-
1127
- return Object.values(results).every(r => r) && containerWorking;
1128
- }
1129
-
1130
- async function checkClusterExists() {
1131
- try {
1132
- // Check if TRIBE namespace exists
1133
- execSync('kubectl get namespace tribe-system', { stdio: 'ignore' });
1134
-
1135
- // Also check if key TRIBE pods are running
1136
- const podsOutput = execSync('kubectl get pods -n tribe-system -o json', {
1137
- encoding: 'utf8',
1138
- stdio: 'pipe'
1139
- });
1140
-
1141
- const pods = JSON.parse(podsOutput);
1142
- if (!pods.items || pods.items.length === 0) {
1143
- return false; // Namespace exists but no pods
1144
- }
1145
-
1146
- // Check if essential pods exist (gitea, postgres, taskmaster)
1147
- const essentialPods = ['gitea', 'postgres', 'taskmaster'];
1148
- const runningPods = pods.items.filter(pod => {
1149
- const podName = pod.metadata.name;
1150
- return essentialPods.some(essential => podName.includes(essential));
1151
- });
1152
-
1153
- return runningPods.length >= essentialPods.length;
1154
- } catch {
1155
- return false;
1156
- }
1157
- }
1158
-
1159
- async function checkColimaRunning() {
1160
- try {
1161
- const colimaPath = await findCommand('colima') || path.join(binDir, 'colima');
1162
- execSync(`${colimaPath} status`, { stdio: 'ignore' });
1163
- return true;
1164
- } catch {
1165
- return false;
1166
- }
1167
- }
1168
-
1169
- async function checkColimaHasKubernetes() {
1170
- try {
1171
- const colimaPath = await findCommand('colima') || path.join(binDir, 'colima');
1172
- const status = execSync(`${colimaPath} status`, { encoding: 'utf8' });
1173
- // Check if kubernetes is mentioned in the status
1174
- return status.toLowerCase().includes('kubernetes');
1175
- } catch {
1176
- return false;
1177
- }
1178
- }
1179
-
1180
- async function startColimaWithKubernetes() {
1181
- const spinner = ora('Starting Colima with Kubernetes...').start();
1182
-
1183
- try {
1184
- // Check if already running
1185
- if (await checkColimaRunning()) {
1186
- // Check if it has Kubernetes enabled
1187
- if (await checkColimaHasKubernetes()) {
1188
- spinner.succeed('Colima is already running with Kubernetes');
1189
-
1190
- // Verify kubectl works
1191
- try {
1192
- execSync('kubectl cluster-info', { stdio: 'ignore' });
1193
- return true;
1194
- } catch {
1195
- spinner.info('Colima is running but kubectl not connected, continuing...');
1196
- }
1197
- } else {
1198
- spinner.text = 'Colima is running without Kubernetes. Restarting with Kubernetes...';
1199
-
1200
- // Stop existing Colima
1201
- const colimaPath = await findCommand('colima') || path.join(binDir, 'colima');
1202
- try {
1203
- execSync(`${colimaPath} stop`, {
1204
- stdio: 'pipe',
1205
- timeout: 30000
1206
- });
1207
- // Wait for it to fully stop
1208
- await new Promise(resolve => setTimeout(resolve, 5000));
1209
- } catch (stopError) {
1210
- log.warning('Failed to stop Colima, attempting to start anyway...');
1211
- }
134
+ // Not critical
1212
135
  }
1213
136
  }
1214
137
 
1215
- // Start Colima with Kubernetes enabled
1216
- spinner.text = 'Starting Colima (this may take a few minutes on first run)...';
1217
- const colimaPath = await findCommand('colima') || path.join(binDir, 'colima');
1218
- execSync(`${colimaPath} start --kubernetes --cpu 4 --memory 8 --disk 20`, {
1219
- stdio: 'pipe',
1220
- env: { ...process.env, PATH: `${binDir}:${process.env.PATH}` },
1221
- timeout: 300000 // 5 minute timeout for first start
1222
- });
1223
-
1224
- // Give Colima a moment to stabilize after starting
1225
- spinner.text = 'Waiting for Colima to stabilize...';
1226
- await new Promise(resolve => setTimeout(resolve, 5000));
1227
-
1228
- // Verify it's working and set context
1229
- const kubectlPath = await findCommand('kubectl') || 'kubectl';
1230
- execSync(`${kubectlPath} version --client`, { stdio: 'ignore' });
1231
-
1232
- // Set kubectl context to colima
1233
- try {
1234
- execSync(`${kubectlPath} config use-context colima`, { stdio: 'ignore' });
1235
- } catch {
1236
- // Context might not exist yet, that's OK
1237
- }
1238
- spinner.succeed('Colima started with Kubernetes');
138
+ spinner.succeed(exists ? 'TRIBE CLI updated' : 'TRIBE CLI installed');
1239
139
  return true;
1240
140
  } catch (error) {
1241
- spinner.fail('Failed to start Colima with Kubernetes');
1242
- log.error(error.message);
141
+ spinner.fail(`Installation failed: ${error.message}`);
1243
142
  return false;
1244
143
  }
1245
144
  }
1246
145
 
1247
- async function deployTribeCluster() {
1248
- const spinner = ora('Deploying TRIBE cluster...').start();
146
+ async function setupPath() {
147
+ const spinner = ora('🔧 Setting up PATH...').start();
1249
148
 
1250
149
  try {
1251
- // Create a marker file to indicate first-run deployment
1252
- const deploymentMarker = path.join(tribeDir, '.cluster-deployed');
1253
-
1254
- // Check if we've already deployed
1255
- if (fs.existsSync(deploymentMarker)) {
1256
- // Check if cluster actually exists
1257
- if (await checkClusterExists()) {
1258
- spinner.succeed('TRIBE cluster already deployed');
1259
- return true;
1260
- }
1261
- // Marker exists but cluster doesn't - remove marker and redeploy
1262
- fs.unlinkSync(deploymentMarker);
1263
- }
1264
-
1265
- // Copy deployment YAML to .tribe directory
1266
- const sourceYaml = path.join(__dirname, 'tribe-deployment.yaml');
1267
- const destYaml = path.join(tribeDir, 'tribe-deployment.yaml');
1268
-
1269
- if (fs.existsSync(sourceYaml)) {
1270
- fs.copyFileSync(sourceYaml, destYaml);
1271
- log.info('Copied deployment configuration');
1272
- } else {
1273
- // Download deployment YAML if not bundled
1274
- spinner.text = 'Downloading deployment configuration...';
1275
- const yamlSources = [
1276
- // Direct from GitHub (primary)
1277
- 'https://raw.githubusercontent.com/TRIBE-INC/0zen/main/sdk/cmd/cli-gui/package/tribe-deployment.yaml',
1278
- // jsDelivr CDN as fallback
1279
- 'https://cdn.jsdelivr.net/gh/TRIBE-INC/0zen@main/sdk/cmd/cli-gui/package/tribe-deployment.yaml'
1280
- ];
1281
-
1282
- let yamlDownloaded = false;
1283
- for (const yamlUrl of yamlSources) {
1284
- try {
1285
- await downloadFile(yamlUrl, destYaml);
1286
- log.info('Downloaded deployment configuration');
1287
- yamlDownloaded = true;
1288
- break;
1289
- } catch (downloadError) {
1290
- // Try next source
1291
- }
1292
- }
1293
-
1294
- if (!yamlDownloaded) {
1295
- spinner.fail('Failed to get deployment configuration');
1296
- throw new Error('Could not download deployment YAML from any source');
1297
- }
1298
- }
1299
-
1300
- // Run tribe start command with deployment YAML path
1301
- spinner.text = 'Running TRIBE cluster deployment...';
1302
- const tribePath = path.join(tribeBinDir, 'tribe');
1303
-
1304
- // Set environment variable for the deployment YAML location
1305
- const env = {
1306
- ...process.env,
1307
- PATH: `${binDir}:${process.env.PATH}`,
1308
- TRIBE_DEPLOYMENT_YAML: destYaml
1309
- };
1310
-
1311
- // Wait for Kubernetes to be fully ready
1312
- spinner.text = 'Waiting for Kubernetes to be ready...';
1313
- const kubectlPath = await findCommand('kubectl') || 'kubectl';
1314
- let apiReady = false;
1315
- let contextSet = false;
1316
-
1317
- // First, wait for the API server to be accessible
1318
- for (let i = 0; i < 30; i++) {
1319
- try {
1320
- // Try to get cluster info - this will work regardless of port
1321
- const clusterInfo = execSync(`${kubectlPath} cluster-info`, {
1322
- encoding: 'utf8',
1323
- env: env,
1324
- stdio: 'pipe'
1325
- });
1326
-
1327
- // Check if we got valid cluster info
1328
- if (clusterInfo && clusterInfo.includes('is running at')) {
1329
- apiReady = true;
1330
- spinner.text = 'Kubernetes API server is ready';
1331
-
1332
- // Extract the actual API server URL for logging
1333
- const urlMatch = clusterInfo.match(/is running at (https?:\/\/[^\s]+)/);
1334
- if (urlMatch) {
1335
- log.info(`Kubernetes API server: ${urlMatch[1]}`);
1336
- }
1337
- break;
1338
- }
1339
- } catch (error) {
1340
- spinner.text = `Waiting for Kubernetes API server... (${i+1}/30)`;
1341
- await new Promise(resolve => setTimeout(resolve, 2000));
1342
- }
1343
- }
150
+ // Create PATH script
151
+ const envScript = `#!/bin/bash
152
+ # TRIBE CLI Environment
153
+ TRIBE_BIN_DIR="$HOME/.tribe/bin"
154
+ if [[ -d "$TRIBE_BIN_DIR" ]] && [[ ":$PATH:" != *":$TRIBE_BIN_DIR:"* ]]; then
155
+ export PATH="$TRIBE_BIN_DIR:$PATH"
156
+ fi`;
1344
157
 
1345
- if (!apiReady) {
1346
- spinner.warn('Kubernetes API server not ready after 60 seconds');
1347
- log.info('Deployment may fail. You can try running "tribe start" manually later.');
1348
- return false;
1349
- }
158
+ const envScriptPath = path.join(tribeDir, 'tribe-env.sh');
159
+ fs.writeFileSync(envScriptPath, envScript);
160
+ fs.chmodSync(envScriptPath, '755');
1350
161
 
1351
- // Set the kubectl context to colima
1352
- spinner.text = 'Setting kubectl context...';
1353
- try {
1354
- execSync(`${kubectlPath} config use-context colima`, {
1355
- stdio: 'ignore',
1356
- env: env
1357
- });
1358
- contextSet = true;
1359
- } catch {
1360
- log.warning('Could not set kubectl context to colima');
1361
- }
162
+ // Try to add to shell config
163
+ const shellConfig = process.env.SHELL?.includes('zsh') ? '.zshrc' : '.bashrc';
164
+ const rcPath = path.join(homeDir, shellConfig);
165
+ const sourceLine = 'source ~/.tribe/tribe-env.sh';
1362
166
 
1363
- // Wait for the node to be ready
1364
- spinner.text = 'Waiting for Kubernetes node to be ready...';
1365
- let nodeReady = false;
1366
- for (let i = 0; i < 20; i++) {
1367
- try {
1368
- // First check if any nodes exist
1369
- const nodeList = execSync(`${kubectlPath} get nodes -o json`, {
1370
- encoding: 'utf8',
1371
- env: env,
1372
- stdio: 'pipe'
1373
- });
1374
-
1375
- const nodes = JSON.parse(nodeList);
1376
- if (nodes.items && nodes.items.length > 0) {
1377
- // Now check if the node is ready
1378
- const nodeStatus = execSync(`${kubectlPath} get nodes -o jsonpath='{.items[0].status.conditions[?(@.type=="Ready")].status}'`, {
1379
- encoding: 'utf8',
1380
- env: env,
1381
- stdio: 'pipe'
1382
- }).trim();
1383
-
1384
- if (nodeStatus === 'True') {
1385
- nodeReady = true;
1386
- spinner.text = 'Kubernetes node is ready';
1387
- break;
1388
- }
1389
- }
1390
- } catch (error) {
1391
- // Ignore errors - node might not be registered yet or connection issues
1392
- if (error.message && error.message.includes('connection reset')) {
1393
- spinner.text = `Waiting for Kubernetes node... (${i+1}/20) - reconnecting...`;
1394
- } else {
1395
- spinner.text = `Waiting for Kubernetes node... (${i+1}/20)`;
1396
- }
167
+ if (fs.existsSync(rcPath)) {
168
+ const content = fs.readFileSync(rcPath, 'utf8');
169
+ if (!content.includes(sourceLine)) {
170
+ fs.appendFileSync(rcPath, `\n# TRIBE CLI\n${sourceLine}\n`);
1397
171
  }
1398
- await new Promise(resolve => setTimeout(resolve, 3000));
1399
- }
1400
-
1401
- if (!nodeReady) {
1402
- spinner.warn('Kubernetes node not ready after 60 seconds');
1403
- log.info('The cluster may still be initializing. Proceeding with deployment...');
1404
172
  }
1405
173
 
1406
- // Small delay to let everything stabilize
1407
- spinner.text = 'Starting TRIBE deployment...';
1408
- await new Promise(resolve => setTimeout(resolve, 2000));
1409
-
1410
- // Execute tribe start
1411
- execSync(`${tribePath} start`, {
1412
- stdio: 'pipe',
1413
- env: env
1414
- });
1415
-
1416
- // Create marker file
1417
- fs.writeFileSync(deploymentMarker, new Date().toISOString());
1418
-
1419
- spinner.succeed('TRIBE cluster deployed successfully');
1420
- log.info('Services are starting up. Check status with: tribe status');
174
+ spinner.succeed('PATH configured');
1421
175
  return true;
1422
176
  } catch (error) {
1423
- spinner.fail('Failed to deploy TRIBE cluster');
1424
- log.error(error.message);
1425
-
1426
- // Check if this is a Kubernetes connectivity issue
1427
- if (error.message && error.message.includes('connection refused')) {
1428
- log.warning('\nIt appears Kubernetes is not ready or not enabled.');
1429
- log.info('Troubleshooting steps:');
1430
- log.info('1. Check Colima status: colima status');
1431
- log.info('2. If running without Kubernetes, restart it:');
1432
- log.info(' colima stop');
1433
- log.info(' colima start --kubernetes');
1434
- log.info('3. Then run: tribe start');
1435
- } else {
1436
- log.info('You can manually deploy later with: tribe start');
1437
- }
177
+ spinner.warn('PATH setup needs manual configuration');
178
+ console.log(chalk.yellow('\nTo use tribe command, run:'));
179
+ console.log(chalk.cyan(' source ~/.tribe/tribe-env.sh\n'));
1438
180
  return false;
1439
181
  }
1440
182
  }
1441
183
 
1442
- async function promptForPathSetup() {
1443
- return new Promise((resolve) => {
1444
- const rl = readline.createInterface({
1445
- input: process.stdin,
1446
- output: process.stdout
1447
- });
1448
-
1449
- console.log('');
1450
- rl.question(chalk.yellow('Would you like to add "tribe" to your PATH? (y/n) '), (answer) => {
1451
- rl.close();
1452
- resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
1453
- });
1454
- });
1455
- }
1456
-
1457
- async function promptForClusterSetup() {
1458
- // Check for auto-approve in CI environments
1459
- if (process.env.CI === 'true' || process.env.TRIBE_AUTO_APPROVE === 'true') {
1460
- console.log('\n' + chalk.bold('🚀 TRIBE Cluster Setup'));
1461
- console.log(chalk.green('Auto-approving cluster setup (CI mode)'));
1462
- return true;
1463
- }
1464
-
1465
- // Simple prompt without external dependencies
1466
- return new Promise((resolve) => {
1467
- console.log('\n' + chalk.bold('🚀 TRIBE Cluster Setup'));
1468
- console.log('\nWould you like to set up the TRIBE cluster now?');
1469
- console.log('This will:');
1470
- console.log(' • Start ' + (platform === 'darwin' ? 'Colima' : 'K3s') + ' with Kubernetes');
1471
- console.log(' • Deploy all TRIBE services');
1472
- console.log(' • Set up port forwarding');
1473
- console.log('\n' + chalk.yellow('Note: This requires ~2GB disk space and may take a few minutes'));
1474
-
1475
- process.stdout.write('\nSet up now? [Y/n]: ');
1476
-
1477
- process.stdin.resume();
1478
- process.stdin.setEncoding('utf8');
1479
- process.stdin.once('data', (data) => {
1480
- process.stdin.pause();
1481
- const answer = data.toString().trim().toLowerCase();
1482
- resolve(answer === '' || answer === 'y' || answer === 'yes');
1483
- });
1484
- });
1485
- }
1486
-
1487
- async function cleanupOldInstallations() {
1488
- // Clean up old aliases
1489
- const rcFiles = ['.zshrc', '.bashrc'];
1490
- rcFiles.forEach(file => {
1491
- const rcPath = path.join(homeDir, file);
1492
- if (fs.existsSync(rcPath)) {
1493
- try {
1494
- let content = fs.readFileSync(rcPath, 'utf8');
1495
- const originalContent = content;
1496
-
1497
- // Remove old tribe-cli aliases
1498
- content = content.replace(/^alias tribe=['"]tribe-cli['"]\s*$/gm, '# $& # Removed by TRIBE installer');
1499
- content = content.replace(/^alias t=['"]tribe-cli['"]\s*$/gm, '# $& # Removed by TRIBE installer');
1500
-
1501
- if (content !== originalContent) {
1502
- fs.writeFileSync(rcPath, content);
1503
- log.info(`Cleaned up old aliases in ${file}`);
1504
- }
1505
- } catch (error) {
1506
- log.warning(`Could not clean up ${file}: ${error.message}`);
1507
- }
1508
- }
1509
- });
1510
- }
1511
-
1512
- async function forceCleanInstallation() {
1513
- console.log(chalk.yellow('\n🧹 Performing force clean installation...\n'));
1514
-
1515
- // Clean up Kubernetes resources first (while cluster is running)
1516
- try {
1517
- execSync('kubectl delete namespace tribe-system --ignore-not-found=true', {
1518
- stdio: 'ignore',
1519
- timeout: 30000
1520
- });
1521
- log.success('Removed TRIBE namespace from cluster');
1522
- } catch (error) {
1523
- // Cluster might not be running or namespace doesn't exist
1524
- }
1525
-
1526
- // Uninstall global npm package if exists
1527
- try {
1528
- execSync('npm uninstall -g tribe-cli-global', {
1529
- stdio: 'ignore'
1530
- });
1531
- log.success('Removed global tribe command');
1532
- } catch (error) {
1533
- // It might not be installed, which is fine
1534
- }
1535
-
1536
- // Clean from old ~/bin location
1537
- const oldBinDir = path.join(homeDir, 'bin');
1538
- const binariesToRemove = ['tribe', 'kubectl', 'colima'];
1539
-
1540
- for (const binary of binariesToRemove) {
1541
- const oldPath = path.join(oldBinDir, binary);
1542
- if (fs.existsSync(oldPath)) {
1543
- try {
1544
- fs.unlinkSync(oldPath);
1545
- log.success(`Removed existing ${binary} binary from ~/bin`);
1546
- } catch (error) {
1547
- log.warning(`Could not remove ${binary}: ${error.message}`);
1548
- }
1549
- }
1550
- }
1551
-
1552
- // Clean from ~/.tribe/bin
1553
- for (const binary of binariesToRemove) {
1554
- const binaryPath = path.join(tribeBinDir, binary);
1555
- if (fs.existsSync(binaryPath)) {
1556
- try {
1557
- fs.unlinkSync(binaryPath);
1558
- log.success(`Removed existing ${binary} binary from ~/.tribe/bin`);
1559
- } catch (error) {
1560
- log.warning(`Could not remove ${binary}: ${error.message}`);
1561
- }
1562
- }
1563
- }
1564
-
1565
- // Clean up TRIBE configuration directory
1566
- if (fs.existsSync(tribeDir)) {
1567
- try {
1568
- // Keep important configs but remove deployment markers
1569
- const deploymentMarker = path.join(tribeDir, '.cluster-deployed');
1570
- if (fs.existsSync(deploymentMarker)) {
1571
- fs.unlinkSync(deploymentMarker);
1572
- log.success('Removed cluster deployment marker');
1573
- }
1574
-
1575
- // Remove cached deployment YAML
1576
- const deploymentYaml = path.join(tribeDir, 'tribe-deployment.yaml');
1577
- if (fs.existsSync(deploymentYaml)) {
1578
- fs.unlinkSync(deploymentYaml);
1579
- log.success('Removed cached deployment configuration');
1580
- }
1581
- } catch (error) {
1582
- log.warning(`Could not clean TRIBE directory: ${error.message}`);
1583
- }
1584
- }
1585
-
1586
- // Clean up npm cache for @_xtribe/cli
1587
- try {
1588
- execSync('npm cache clean --force @_xtribe/cli', {
1589
- stdio: 'ignore',
1590
- timeout: 10000
1591
- });
1592
- log.success('Cleared npm cache for @_xtribe/cli');
1593
- } catch (error) {
1594
- // npm cache clean might fail, but that's okay
1595
- log.info('npm cache clean attempted (may have failed, which is okay)');
1596
- }
1597
-
1598
- // Remove first-install marker to allow auto-launch
1599
- const firstInstallMarker = path.join(tribeDir, '.first-install-complete');
1600
- if (fs.existsSync(firstInstallMarker)) {
1601
- try {
1602
- fs.unlinkSync(firstInstallMarker);
1603
- log.success('Removed first-install marker');
1604
- } catch (error) {
1605
- log.warning(`Could not remove first-install marker: ${error.message}`);
1606
- }
1607
- }
1608
-
1609
- // Platform-specific cleanup
1610
- if (platform === 'darwin') {
1611
- // Check if Colima is running and stop it
1612
- try {
1613
- const colimaPath = await findCommand('colima');
1614
- if (colimaPath) {
1615
- const status = execSync(`${colimaPath} status 2>&1`, { encoding: 'utf8' });
1616
- if (status.includes('is running')) {
1617
- log.info('Stopping Colima...');
1618
- execSync(`${colimaPath} stop`, { stdio: 'ignore', timeout: 30000 });
1619
- log.success('Stopped Colima');
1620
- }
1621
- }
1622
- } catch (error) {
1623
- // Colima might not be installed or running
1624
- }
1625
- } else if (platform === 'linux') {
1626
- // Note about K3s - requires manual uninstall
1627
- try {
1628
- execSync('which k3s', { stdio: 'ignore' });
1629
- log.warning('K3s detected. To fully remove K3s, run: /usr/local/bin/k3s-uninstall.sh');
1630
- log.info('The installer will proceed, but K3s will remain installed');
1631
- } catch {
1632
- // K3s not installed
1633
- }
1634
- }
1635
-
1636
- console.log(chalk.green('\n✓ Clean installation preparation complete\n'));
1637
- }
1638
-
1639
184
  async function main() {
1640
- console.log(chalk.bold.blue('\n🚀 TRIBE CLI Installer\n'));
1641
-
1642
- // Detect and display platform info
1643
- const displayArch = process.arch === 'x64' ? 'amd64' : process.arch;
1644
- console.log(chalk.gray(`Platform: ${platform} (${displayArch})`));
1645
- console.log(chalk.gray(`Node: ${process.version}`));
1646
- console.log('');
1647
-
1648
- // Check for unsupported platforms early
1649
- if (platform === 'win32') {
1650
- console.log(chalk.yellow('\n⚠️ Windows is not yet supported'));
1651
- console.log(chalk.yellow('Please use WSL2 with Ubuntu and run this installer inside WSL2'));
1652
- console.log(chalk.yellow('Instructions: https://docs.microsoft.com/en-us/windows/wsl/install\n'));
1653
- process.exit(1);
1654
- }
1655
-
1656
- // Edge case detection
1657
- // 1. Check for unsupported architectures
1658
- const supportedArchitectures = ['x64', 'x86_64', 'arm64', 'aarch64'];
1659
- if (!supportedArchitectures.includes(nodeArch)) {
1660
- console.log(chalk.red('\n❌ Unsupported Architecture'));
1661
- console.log(chalk.red(`Your system architecture (${nodeArch}) is not supported.`));
1662
- console.log(chalk.yellow('TRIBE only provides binaries for x64 (AMD64) and ARM64.\n'));
1663
- process.exit(1);
1664
- }
1665
-
1666
- // 2. WSL2 detection
1667
- if (fs.existsSync('/proc/sys/fs/binfmt_misc/WSLInterop') || process.env.WSL_DISTRO_NAME) {
1668
- console.log(chalk.yellow('\n⚠️ WSL2 Environment Detected'));
1669
- console.log(chalk.yellow('K3s may have networking limitations in WSL2.'));
1670
- console.log(chalk.yellow('Consider using Docker Desktop with WSL2 backend instead.\n'));
1671
- // Don't exit, just warn
1672
- }
1673
-
1674
- // 3. Proxy detection
1675
- const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
1676
- if (proxyUrl) {
1677
- console.log(chalk.blue('ℹ️ Proxy detected: ' + proxyUrl));
1678
- console.log(chalk.blue('Downloads will use system proxy settings.\n'));
1679
- }
1680
-
1681
- // 4. Check for existing Kubernetes installations
1682
- const k8sTools = [];
1683
- const checkTools = ['minikube', 'microk8s', 'k3d', 'kubectl'];
1684
- for (const tool of checkTools) {
1685
- try {
1686
- execSync(`which ${tool}`, { stdio: 'ignore' });
1687
- k8sTools.push(tool);
1688
- } catch {}
1689
- }
1690
-
1691
- if (k8sTools.length > 0) {
1692
- console.log(chalk.blue('\n📦 Existing Kubernetes tools detected: ' + k8sTools.join(', ')));
1693
- console.log(chalk.blue('You can use --skip-cluster to skip K3s installation.\n'));
1694
- }
1695
-
1696
- // 5. Disk space check
1697
- try {
1698
- const stats = fs.statfsSync(homeDir);
1699
- const freeGB = (stats.bavail * stats.bsize) / (1024 ** 3);
1700
- if (freeGB < 3) {
1701
- console.log(chalk.yellow(`\n⚠️ Low disk space: ${freeGB.toFixed(1)}GB free`));
1702
- console.log(chalk.yellow('K3s installation requires at least 3GB of free space.\n'));
1703
- }
1704
- } catch {}
1705
-
1706
- // Clean up old installations
1707
- await cleanupOldInstallations();
1708
-
1709
- log.info(`Detected: ${platform} (${arch})`);
1710
- log.info(`Installing to: ${tribeBinDir}`);
1711
-
1712
- // No longer updating PATH - will use npm global install instead
1713
-
1714
- const tasks = [];
185
+ const args = process.argv.slice(2);
1715
186
 
1716
- // Platform-specific Kubernetes installation
1717
- if (platform === 'darwin') {
1718
- tasks.push({ name: 'Colima', fn: installColima });
1719
- } else if (platform === 'linux') {
1720
- tasks.push({ name: 'K3s', fn: installK3s });
187
+ // Handle help
188
+ if (args.includes('--help') || args.includes('-h')) {
189
+ console.log(chalk.bold('TRIBE Telemetry Installer\n'));
190
+ console.log('Empower your AI coding agents and become 10x faster\n');
191
+ console.log('Usage: npx @_xtribe/cli [options]\n');
192
+ console.log('Options:');
193
+ console.log(' --help, -h Show this help message');
194
+ console.log(' --version Show version');
195
+ process.exit(0);
1721
196
  }
1722
197
 
1723
- // Common tools for all platforms
1724
- tasks.push(
1725
- { name: 'kubectl', fn: installKubectl },
1726
- { name: 'TRIBE CLI', fn: installTribeCLI },
1727
- { name: 'PATH environment', fn: setupPathEnvironment }
1728
- // We'll try npm global as a fallback, but not as primary method
1729
- );
1730
-
1731
- let allSuccess = true;
1732
-
1733
- for (const task of tasks) {
1734
- log.step(`Installing ${task.name}...`);
1735
- const success = await task.fn();
1736
- if (!success) {
1737
- allSuccess = false;
1738
- if (task.name === 'PATH environment') {
1739
- global.envScriptCreated = false;
1740
- }
1741
- }
1742
- }
198
+ // Show welcome
199
+ showWelcome();
1743
200
 
1744
- // Verify everything first
1745
- const verified = await verifyInstallation();
201
+ // Simple installation flow
202
+ console.log(chalk.bold('📦 Setting up TRIBE Telemetry...\n'));
1746
203
 
1747
- // If critical components failed (especially TRIBE CLI), exit with error
1748
- if (!allSuccess) {
1749
- // Check specifically if TRIBE CLI installation failed
1750
- const tribeBinaryExists = fs.existsSync(path.join(tribeBinDir, 'tribe'));
1751
- if (!tribeBinaryExists) {
1752
- console.error(chalk.red('\n❌ Installation failed: TRIBE CLI could not be installed'));
1753
- process.exit(1);
1754
- }
204
+ // Install TRIBE CLI
205
+ const success = await installTribeCLI();
206
+ if (!success) {
207
+ console.error(chalk.red('\n❌ Installation failed'));
208
+ console.log(chalk.yellow('Please check your internet connection and try again'));
209
+ process.exit(1);
1755
210
  }
1756
211
 
1757
- // Try to start container runtime (after showing results)
1758
- const runtimeStarted = await startContainerRuntime();
212
+ // Setup PATH
213
+ await setupPath();
1759
214
 
1760
- // If we successfully started the runtime, we should continue with deployment
1761
- if (verified || runtimeStarted) {
1762
- // Check if cluster already exists
1763
- const clusterExists = await checkClusterExists();
1764
-
1765
- if (!clusterExists && !global.skipCluster) {
1766
- // If we just started Colima, automatically deploy the cluster
1767
- // Don't prompt if we just started the runtime - deploy automatically
1768
- const shouldSetup = runtimeStarted ? true : await promptForClusterSetup();
1769
-
1770
- if (shouldSetup) {
1771
- console.log('');
1772
-
1773
- // Start Colima with Kubernetes if on macOS (skip if we just started it)
1774
- if (platform === 'darwin' && !runtimeStarted) {
1775
- const colimaStarted = await startColimaWithKubernetes();
1776
- if (!colimaStarted) {
1777
- log.error('Failed to start Colima. Please run manually:');
1778
- console.log(' colima start --kubernetes');
1779
- console.log(' tribe start');
1780
- process.exit(1);
1781
- }
1782
- }
1783
-
1784
- // Deploy TRIBE cluster
1785
- const deployed = await deployTribeCluster();
1786
-
1787
- if (deployed) {
1788
- console.log('\n' + chalk.bold.green('✨ TRIBE is ready!'));
1789
- console.log('');
1790
-
1791
- // PATH was already set up automatically in setupPathEnvironment
1792
- // No need to prompt again
1793
-
1794
- // Provide immediate access to tribe command
1795
- if (global.pathSetupComplete) {
1796
- log.info('PATH configuration updated automatically!');
1797
- console.log('');
1798
- console.log(chalk.bold('✨ TRIBE is ready to use!'));
1799
- console.log('');
1800
- console.log(chalk.yellow(' Current terminal: ') + chalk.green('source ~/.tribe/tribe-env.sh'));
1801
- console.log(chalk.yellow(' New terminal: ') + chalk.green('tribe'));
1802
- } else if (global.envScriptCreated) {
1803
- log.info('TRIBE command is installed!');
1804
- console.log('');
1805
- console.log(chalk.bold('To add "tribe" to your PATH:'));
1806
- console.log(chalk.green(' npx @_xtribe/cli setup-path'));
1807
- console.log('');
1808
- console.log(chalk.bold('Or for this session only:'));
1809
- console.log(chalk.green(' source ~/.tribe/tribe-env.sh'));
1810
- console.log('');
1811
- console.log(chalk.bold('Or use the full path:'));
1812
- console.log(chalk.green(' ~/.tribe/bin/tribe'));
1813
- } else {
1814
- log.info('TRIBE command is installed!');
1815
- console.log(chalk.green(' ~/.tribe/bin/tribe # Run TRIBE'));
1816
- console.log(chalk.gray(' ~/.tribe/bin/tribe --help # View available commands'));
1817
- console.log(chalk.gray(' ~/.tribe/bin/tribe status # Check cluster status'));
1818
- }
1819
- console.log('');
1820
-
1821
- log.info('Quick start:');
1822
- console.log(' tribe # Launch interactive CLI');
1823
- console.log(' tribe status # Check cluster status');
1824
- console.log(' tribe create-task # Create a new task');
1825
- console.log('');
1826
- log.info('First time? The CLI will guide you through creating your first project!');
1827
- } else {
1828
- log.warning('Cluster deployment failed, but you can try manually:');
1829
- console.log(' tribe start');
1830
- }
1831
- } else {
1832
- console.log('\n' + chalk.bold('Setup Complete!'));
1833
- log.info('You can set up the cluster later with:');
1834
- console.log(' tribe start');
1835
- }
1836
- } else {
1837
- console.log('\n' + chalk.bold.green('✨ TRIBE is ready!'));
1838
- log.success('Cluster is already running');
1839
- console.log('');
1840
-
1841
- // Check if PATH was automatically configured
1842
- if (global.pathSetupComplete) {
1843
- log.info('PATH configured automatically!');
1844
- console.log('');
1845
- console.log(chalk.bold('TRIBE commands are available:'));
1846
- console.log(chalk.yellow(' Current terminal: ') + chalk.green('source ~/.tribe/tribe-env.sh'));
1847
- console.log(chalk.yellow(' New terminals: ') + chalk.green('tribe (works automatically)'));
1848
- } else if (global.envScriptCreated) {
1849
- log.info('TRIBE command is installed!');
1850
- console.log('');
1851
- console.log(chalk.bold('To use "tribe" command from anywhere:'));
1852
- console.log(chalk.green(' source ~/.tribe/tribe-env.sh'));
1853
- console.log('');
1854
- console.log(chalk.bold('Or you can use the full path:'));
1855
- console.log(chalk.green(' ~/.tribe/bin/tribe # Run TRIBE'));
1856
- } else {
1857
- log.info('TRIBE command is installed!');
1858
- console.log(chalk.green(' ~/.tribe/bin/tribe # Run TRIBE'));
1859
- }
1860
- console.log('');
1861
-
1862
- log.info('Commands:');
1863
- console.log(' tribe # Launch interactive CLI');
1864
- console.log(' tribe status # Check status');
1865
- console.log(' tribe create-task # Create a new task');
1866
- }
1867
- } else {
1868
- console.log('\n' + chalk.bold('Next Steps:'));
1869
- log.warning('Some components need attention:');
1870
- console.log('');
1871
- // Check container runtime for guidance
1872
- let runtimeWorking = false;
1873
- if (platform === 'darwin') {
1874
- try {
1875
- const colimaStatus = execSync('colima status 2>&1', { encoding: 'utf8' });
1876
- runtimeWorking = colimaStatus.includes('is running');
1877
- } catch {
1878
- runtimeWorking = false;
1879
- }
1880
-
1881
- if (!runtimeWorking) {
1882
- log.info('Start container runtime:');
1883
- console.log(' colima start --kubernetes # Start Colima with Kubernetes');
1884
- }
1885
- } else if (platform === 'linux') {
1886
- try {
1887
- execSync('systemctl is-active k3s', { stdio: 'ignore' });
1888
- runtimeWorking = true;
1889
- } catch {
1890
- runtimeWorking = false;
1891
- }
1892
-
1893
- if (!runtimeWorking) {
1894
- log.info('Start container runtime:');
1895
- console.log(' sudo systemctl start k3s # Start k3s');
1896
- }
1897
- }
1898
- console.log('');
1899
- log.info('Restart your shell or run: source ~/.zshrc');
1900
- log.info('Then run: tribe start');
1901
- }
215
+ // Show summary
216
+ showInstallationSummary();
1902
217
 
1903
- // Auto-launch handling (only for successful installations)
1904
- const checks = global.installationChecks || {};
1905
- if (checks.kubectl && checks.tribe) {
1906
- try {
1907
- const autoLaunch = require('./install-tribe-autolaunch.js');
1908
- if (autoLaunch.shouldAutoLaunch()) {
1909
- console.log(''); // Spacing before auto-launch
1910
- await autoLaunch.handleAutoLaunch();
1911
- // If auto-launch takes over, we won't reach here
1912
- }
1913
- } catch (error) {
1914
- // Silently ignore auto-launch errors to not break the installer
1915
- // Auto-launch is a nice-to-have feature, not critical
1916
- }
1917
- }
218
+ // Final message
219
+ console.log(chalk.green.bold('🎉 Ready to track your AI agents!\n'));
220
+ console.log('Start now with: ' + chalk.cyan.bold('tribe login'));
221
+ console.log();
1918
222
 
1919
- // Exit cleanly
1920
223
  process.exit(0);
1921
224
  }
1922
225
 
1923
- // Handle fetch polyfill for older Node versions
1924
- if (!global.fetch) {
1925
- global.fetch = require('node-fetch');
1926
- }
226
+ // Export for use from index.js
227
+ module.exports = { main };
1927
228
 
229
+ // Run if executed directly
1928
230
  if (require.main === module) {
1929
- const args = process.argv.slice(2);
1930
-
1931
- // Parse version/channel flags
1932
- let versionChannel = 'latest'; // default to latest stable
1933
- const versionArg = args.find(arg => arg.startsWith('--version='));
1934
- const channelArg = args.find(arg => arg.startsWith('--channel='));
1935
-
1936
- if (versionArg) {
1937
- versionChannel = versionArg.split('=')[1];
1938
- } else if (channelArg) {
1939
- versionChannel = channelArg.split('=')[1];
1940
- } else if (args.includes('--beta')) {
1941
- versionChannel = 'beta';
1942
- } else if (args.includes('--latest')) {
1943
- versionChannel = 'latest';
1944
- }
1945
-
1946
- // Store globally for use in download functions
1947
- global.versionChannel = versionChannel;
1948
-
1949
- if (args.includes('--help') || args.includes('-h')) {
1950
- console.log(chalk.bold.blue('TRIBE CLI Installer\n'));
1951
- console.log('Usage: npx @_xtribe/cli [options]\n');
1952
- console.log('Options:');
1953
- console.log(' --help, -h Show this help message');
1954
- console.log(' --verify Only verify existing installation');
1955
- console.log(' --dry-run Show what would be installed');
1956
- console.log(' --skip-cluster Skip cluster deployment (for testing)');
1957
- console.log(' --no-start Alias for --skip-cluster (for CI environments)');
1958
- console.log(' --force Force clean installation (removes existing installations)');
1959
- console.log(' --latest Install latest stable release (default)');
1960
- console.log(' --beta Install latest beta release');
1961
- console.log(' --version=TAG Install specific version tag');
1962
- console.log(' --channel=CHANNEL Install from specific channel (latest/beta)');
1963
- console.log('\nVersion Examples:');
1964
- console.log(' npx @_xtribe/cli@latest # Latest stable (equivalent to --latest)');
1965
- console.log(' npx @_xtribe/cli@beta # Latest beta');
1966
- console.log(' npx @_xtribe/cli --beta # Latest beta (flag version)');
1967
- console.log(' npx @_xtribe/cli --version=v1.2.3 # Specific version');
1968
- console.log('\nThis installer sets up the complete TRIBE development environment:');
1969
-
1970
- if (platform === 'darwin') {
1971
- console.log('• Colima (Container runtime with Kubernetes)');
1972
- } else if (platform === 'linux') {
1973
- console.log('• K3s (Lightweight Kubernetes, one-line install)');
1974
- } else if (platform === 'win32') {
1975
- console.log('\n⚠️ Windows support coming soon!');
1976
- console.log('Please use WSL2 with Ubuntu and run this installer inside WSL2');
1977
- process.exit(1);
1978
- }
1979
-
1980
- console.log('• kubectl (Kubernetes CLI)');
1981
- console.log('• TRIBE CLI (Multi-agent orchestration)');
1982
- console.log('\nAfter installation:');
1983
- console.log(' tribe start # Start TRIBE cluster');
1984
- console.log(' tribe status # Check cluster status');
1985
- process.exit(0);
1986
- }
1987
-
1988
- if (args.includes('--verify')) {
1989
- console.log(chalk.bold.blue('🔍 Verifying TRIBE Installation\n'));
1990
- verifyInstallation().then(success => {
1991
- process.exit(success ? 0 : 1);
1992
- });
1993
- return;
1994
- }
1995
-
1996
- if (args.includes('--dry-run')) {
1997
- console.log(chalk.bold.blue('🔍 TRIBE CLI Installation Preview\n'));
1998
- log.info(`Platform: ${platform} (${arch})`);
1999
- log.info(`Install directory: ${binDir}`);
2000
- log.info(`Version channel: ${versionChannel}`);
2001
- console.log('\nWould install:');
2002
- console.log('• Colima - Container runtime with Kubernetes (macOS only)');
2003
- console.log('• kubectl - Kubernetes CLI');
2004
- console.log('• TRIBE CLI - Multi-agent system');
2005
- process.exit(0);
2006
- }
2007
-
2008
- // Store skip-cluster flag (--no-start is an alias)
2009
- global.skipCluster = args.includes('--skip-cluster') || args.includes('--no-start');
2010
-
2011
- // Check for force flag
2012
- if (args.includes('--force')) {
2013
- forceCleanInstallation().then(() => {
2014
- main().catch(error => {
2015
- console.error(chalk.red('Installation failed:'), error.message);
2016
- process.exit(1);
2017
- });
2018
- }).catch(error => {
2019
- console.error(chalk.red('Force clean failed:'), error.message);
2020
- process.exit(1);
2021
- });
2022
- } else {
2023
- main().catch(error => {
2024
- console.error(chalk.red('Installation failed:'), error.message);
2025
- process.exit(1);
2026
- });
2027
- }
231
+ main().catch(error => {
232
+ console.error(chalk.red('Error:'), error.message);
233
+ process.exit(1);
234
+ });
2028
235
  }
2029
-
2030
- module.exports = { main };