@_xtribe/cli 1.0.0-beta.19 → 1.0.0-beta.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/install-tribe.js CHANGED
@@ -8,9 +8,13 @@ const { execSync, spawn } = require('child_process');
8
8
  const chalk = require('chalk');
9
9
  const ora = require('ora');
10
10
  const which = require('which');
11
+ const fetch = require('node-fetch');
12
+ const crypto = require('crypto'); // Added for checksum verification
11
13
 
12
14
  const platform = os.platform();
13
- const arch = os.arch();
15
+ // Map Node.js arch to standard naming (x64 -> amd64, etc.)
16
+ const nodeArch = os.arch();
17
+ const arch = nodeArch === 'x64' ? 'amd64' : (nodeArch === 'arm64' || nodeArch === 'aarch64' ? 'arm64' : nodeArch);
14
18
  const homeDir = os.homedir();
15
19
  const binDir = path.join(homeDir, 'bin');
16
20
  const tribeDir = path.join(homeDir, '.tribe');
@@ -83,8 +87,25 @@ async function downloadFile(url, dest) {
83
87
  return;
84
88
  }
85
89
  response.pipe(file);
86
- file.on('finish', () => {
90
+ file.on('finish', async () => {
87
91
  file.close();
92
+
93
+ // Check if this might be a pointer file
94
+ const stats = fs.statSync(dest);
95
+ if (stats.size < 100) {
96
+ const content = fs.readFileSync(dest, 'utf8').trim();
97
+ // Check if content looks like a path (e.g., "main-121/tribe-linux-amd64")
98
+ if (content.match(/^[\w\-\/]+$/) && content.includes('/')) {
99
+ console.log(`📎 Following pointer to: ${content}`);
100
+ const baseUrl = url.substring(0, url.lastIndexOf('/'));
101
+ const actualBinaryUrl = `${baseUrl}/${content}`;
102
+
103
+ // Download the actual binary
104
+ fs.unlinkSync(dest); // Remove pointer file
105
+ await downloadFile(actualBinaryUrl, dest);
106
+ }
107
+ }
108
+
88
109
  resolve();
89
110
  });
90
111
  }).on('error', reject);
@@ -93,6 +114,96 @@ async function downloadFile(url, dest) {
93
114
 
94
115
  // Docker installation removed - Colima provides Docker runtime
95
116
 
117
+ async function installK3s() {
118
+ const spinner = ora('Installing K3s (lightweight Kubernetes)...').start();
119
+
120
+ try {
121
+ // Check if K3s is already installed
122
+ try {
123
+ execSync('which k3s', { stdio: 'ignore' });
124
+ spinner.succeed('K3s already installed');
125
+ return true;
126
+ } catch {
127
+ // Not installed, continue
128
+ }
129
+
130
+ // Check if we have permission to install
131
+ const needsSudo = process.getuid && process.getuid() !== 0;
132
+
133
+ // Check if we're in a container or CI environment
134
+ const isContainer = process.env.container === 'docker' ||
135
+ fs.existsSync('/.dockerenv') ||
136
+ !fs.existsSync('/run/systemd/system') ||
137
+ process.env.CI === 'true';
138
+
139
+ if (isContainer) {
140
+ spinner.warn('Running in container/CI - K3s installation skipped');
141
+ log.info('K3s requires systemd and privileged access');
142
+ log.info('For containers, consider using KIND or external cluster');
143
+ return true; // Don't fail in containers
144
+ }
145
+
146
+ // Download and install K3s
147
+ spinner.text = 'Downloading K3s installer...';
148
+
149
+ const installCommand = needsSudo ?
150
+ 'curl -sfL https://get.k3s.io | sudo sh -s - --write-kubeconfig-mode 644' :
151
+ 'curl -sfL https://get.k3s.io | sh -s - --write-kubeconfig-mode 644';
152
+
153
+ try {
154
+ execSync(installCommand, {
155
+ stdio: 'pipe',
156
+ env: { ...process.env, INSTALL_K3S_SKIP_START: 'false' }
157
+ });
158
+
159
+ spinner.succeed('K3s installed successfully');
160
+
161
+ // Wait for K3s to be ready
162
+ spinner.text = 'Waiting for K3s to start...';
163
+ await new Promise(resolve => setTimeout(resolve, 10000));
164
+
165
+ // Configure kubectl to use K3s
166
+ const k3sConfig = '/etc/rancher/k3s/k3s.yaml';
167
+ const userConfig = path.join(os.homedir(), '.kube', 'config');
168
+
169
+ if (fs.existsSync(k3sConfig)) {
170
+ fs.mkdirSync(path.dirname(userConfig), { recursive: true });
171
+
172
+ if (needsSudo) {
173
+ execSync(`sudo cp ${k3sConfig} ${userConfig} && sudo chown $(id -u):$(id -g) ${userConfig}`, { stdio: 'ignore' });
174
+ } else {
175
+ fs.copyFileSync(k3sConfig, userConfig);
176
+ }
177
+
178
+ // Update server address to use localhost
179
+ let configContent = fs.readFileSync(userConfig, 'utf8');
180
+ configContent = configContent.replace(/server: https:\/\/127\.0\.0\.1:6443/, 'server: https://localhost:6443');
181
+ fs.writeFileSync(userConfig, configContent);
182
+
183
+ spinner.succeed('K3s configured');
184
+ }
185
+
186
+ return true;
187
+ } catch (error) {
188
+ if (error.message.includes('permission denied') || error.message.includes('sudo')) {
189
+ spinner.fail('K3s installation requires sudo permission');
190
+ log.info('Please run the installer with sudo or install K3s manually:');
191
+ log.info(' curl -sfL https://get.k3s.io | sudo sh -');
192
+ return false;
193
+ }
194
+ throw error;
195
+ }
196
+
197
+ } catch (error) {
198
+ spinner.fail('K3s installation failed');
199
+ log.error(error.message);
200
+ log.info('Manual K3s installation:');
201
+ log.info(' curl -sfL https://get.k3s.io | sudo sh -');
202
+ log.info(' sudo systemctl enable --now k3s');
203
+ return false;
204
+ }
205
+ }
206
+
96
207
  async function installColima() {
97
208
  if (platform !== 'darwin') {
98
209
  log.info('Colima is only needed on macOS - skipping');
@@ -149,6 +260,23 @@ async function installColima() {
149
260
  // KIND installation removed - using Colima's built-in Kubernetes
150
261
 
151
262
  async function installKubectl() {
263
+ // Linux with K3s already has kubectl
264
+ if (platform === 'linux') {
265
+ try {
266
+ execSync('k3s kubectl version --client', { stdio: 'ignore' });
267
+ log.success('kubectl available via k3s');
268
+ // Create a symlink for convenience
269
+ try {
270
+ execSync('sudo ln -sf /usr/local/bin/k3s /usr/local/bin/kubectl', { stdio: 'ignore' });
271
+ } catch {
272
+ // Link might already exist
273
+ }
274
+ return true;
275
+ } catch {
276
+ // Continue with regular kubectl installation
277
+ }
278
+ }
279
+
152
280
  if (await checkCommand('kubectl')) {
153
281
  log.success('kubectl already installed');
154
282
  return true;
@@ -174,141 +302,286 @@ async function installKubectl() {
174
302
  }
175
303
  }
176
304
 
177
- async function installTribeCli() {
305
+ async function installTribeCLI() {
178
306
  const spinner = ora('Installing TRIBE CLI...').start();
179
307
 
180
308
  try {
181
309
  const tribeDest = path.join(binDir, 'tribe');
182
310
 
183
- // First check if we have a bundled binary
184
- const bundledBinary = path.join(__dirname, 'tribe');
185
- if (fs.existsSync(bundledBinary)) {
186
- fs.copyFileSync(bundledBinary, tribeDest);
187
- fs.chmodSync(tribeDest, '755');
188
- spinner.succeed('TRIBE CLI installed from bundled binary');
189
- return true;
190
- }
191
-
192
- // Check if we have local source
193
- const sourceFile = path.join(__dirname, 'cluster-cli.go');
194
- if (fs.existsSync(sourceFile)) {
195
- // Build from source
311
+ // Skip if already installed and working
312
+ if (fs.existsSync(tribeDest)) {
196
313
  try {
197
- execSync('go version', { stdio: 'ignore' });
198
- execSync(`cd ${__dirname} && go build -o tribe cluster-cli.go client.go`);
199
- fs.copyFileSync(path.join(__dirname, 'tribe'), tribeDest);
200
- fs.chmodSync(tribeDest, '755');
201
- spinner.succeed('TRIBE CLI built from source');
314
+ execSync(`${tribeDest} version`, { stdio: 'ignore' });
315
+ spinner.succeed('TRIBE CLI already installed');
202
316
  return true;
203
317
  } catch {
204
- spinner.warn('Go not available, trying pre-built binary...');
318
+ // Binary exists but doesn't work, remove it
319
+ fs.unlinkSync(tribeDest);
205
320
  }
206
321
  }
207
322
 
208
- // Try pre-built binary from GitHub
209
- const tribeUrl = `https://github.com/0zen/0zen/releases/latest/download/tribe-${platform}-${arch}`;
323
+ // Download pre-built binary from GitHub
324
+ // Proper architecture detection for all platforms
325
+ let arch;
326
+ if (process.arch === 'x64' || process.arch === 'x86_64') {
327
+ arch = 'amd64';
328
+ } else if (process.arch === 'arm64' || process.arch === 'aarch64') {
329
+ arch = 'arm64';
330
+ } else {
331
+ arch = process.arch; // fallback
332
+ }
333
+
334
+ const platform = os.platform();
335
+
336
+ // Multiple sources for reliability
337
+ // For testing, use a direct URL to our releases folder
338
+ const isTestEnv = process.env.TRIBE_TEST_BINARY_URL;
339
+ const githubRepo = process.env.TRIBE_INSTALLER_REPO || 'TRIBE-INC/releases';
210
340
 
341
+ let downloadUrl = '';
342
+
211
343
  try {
212
- await downloadFile(tribeUrl, tribeDest);
213
- fs.chmodSync(tribeDest, '755');
214
- spinner.succeed('TRIBE CLI installed');
215
- return true;
216
- } catch {
217
- // Fallback: look for any existing tribe binary
218
- const possiblePaths = [
219
- path.join(__dirname, '..', 'tribe-cli'),
220
- path.join(__dirname, '..', 'tribe'),
221
- './tribe-cli',
222
- './tribe'
223
- ];
224
-
225
- for (const possiblePath of possiblePaths) {
226
- if (fs.existsSync(possiblePath)) {
227
- fs.copyFileSync(possiblePath, tribeDest);
228
- fs.chmodSync(tribeDest, '755');
229
- spinner.succeed('TRIBE CLI installed from local binary');
230
- return true;
344
+ spinner.text = 'Fetching latest release information from GitHub...';
345
+ const response = await fetch(`https://api.github.com/repos/${githubRepo}/releases`);
346
+ if (!response.ok) {
347
+ throw new Error(`GitHub API returned ${response.status}`);
348
+ }
349
+ const releases = await response.json();
350
+ if (!releases || releases.length === 0) {
351
+ throw new Error('No releases found');
352
+ }
353
+ const latestRelease = releases[0];
354
+ const assetName = `tribe-${platform}-${arch}`;
355
+ const asset = latestRelease.assets.find(a => a.name === assetName);
356
+
357
+ if (!asset) {
358
+ throw new Error(`No binary found for ${platform}-${arch} in release ${latestRelease.tag_name}`);
359
+ }
360
+
361
+ downloadUrl = asset.browser_download_url;
362
+ spinner.text = `Found latest release: ${latestRelease.tag_name}`;
363
+ } catch (error) {
364
+ spinner.fail(`Failed to get release info: ${error.message}`);
365
+ // Fallback to other methods if the API fails
366
+ }
367
+
368
+ const sources = isTestEnv ? [isTestEnv] : [
369
+ downloadUrl,
370
+ // Public releases repository (primary source)
371
+ `https://raw.githubusercontent.com/${githubRepo}/main/cli/latest-tribe-${platform}-${arch}`,
372
+ // GitHub releases (fallback)
373
+ `https://github.com/${githubRepo}/releases/latest/download/tribe-${platform}-${arch}`,
374
+ // Try jsDelivr CDN (for better availability)
375
+ `https://cdn.jsdelivr.net/gh/${githubRepo}@main/cli/latest-tribe-${platform}-${arch}`,
376
+ ].filter(Boolean); // Filter out empty downloadUrl if API failed
377
+
378
+ let downloaded = false;
379
+ let lastError;
380
+
381
+ for (const url of sources) {
382
+ try {
383
+ let downloadUrl = url;
384
+
385
+ // Check if this is the GitHub API endpoint
386
+ if (url.includes('/api.github.com/')) {
387
+ spinner.text = 'Fetching latest release information from GitHub...';
388
+
389
+ // Fetch release data
390
+ const response = await fetch(url);
391
+ if (!response.ok) {
392
+ throw new Error(`GitHub API returned ${response.status}`);
393
+ }
394
+
395
+ const releaseData = await response.json();
396
+
397
+ // Handle both single release and array of releases
398
+ const release = Array.isArray(releaseData) ? releaseData[0] : releaseData;
399
+
400
+ if (!release || !release.assets) {
401
+ throw new Error('No release found');
402
+ }
403
+
404
+ const assetName = `tribe-${platform}-${arch}`;
405
+ const asset = release.assets.find(a => a.name === assetName);
406
+
407
+ if (!asset) {
408
+ throw new Error(`No binary found for ${platform}-${arch}`);
409
+ }
410
+
411
+ downloadUrl = asset.browser_download_url;
412
+ spinner.text = `Found ${release.prerelease ? 'pre-release' : 'release'}: ${release.tag_name}`;
413
+ }
414
+
415
+ spinner.text = `Downloading TRIBE CLI from ${new URL(downloadUrl).hostname}...`;
416
+ await downloadFile(downloadUrl, tribeDest);
417
+
418
+ // Check if file is not empty
419
+ const stats = fs.statSync(tribeDest);
420
+ if (stats.size === 0) {
421
+ throw new Error('Downloaded file is empty');
422
+ }
423
+
424
+ fs.chmodSync(tribeDest, '755');
425
+
426
+ // Verify the binary works
427
+ try {
428
+ execSync(`${tribeDest} version`, { stdio: 'ignore' });
429
+ } catch (versionError) {
430
+ // If version fails, still consider it downloaded if file is valid
431
+ log.warning('Binary downloaded but version check failed - may need different architecture');
231
432
  }
433
+
434
+ spinner.succeed('TRIBE CLI installed successfully');
435
+ downloaded = true;
436
+ break;
437
+ } catch (error) {
438
+ lastError = error;
439
+ // Clean up failed download
440
+ if (fs.existsSync(tribeDest)) {
441
+ fs.unlinkSync(tribeDest);
442
+ }
443
+ log.warning(`Failed: ${error.message}. Trying next source...`);
232
444
  }
445
+ }
233
446
 
234
- throw new Error('No TRIBE CLI binary available');
447
+ if (!downloaded) {
448
+ throw new Error(`Failed to download TRIBE CLI. Please check your internet connection and try again.
449
+
450
+ If this persists, the binaries may not be available for your platform (${platform}-${arch}).
451
+ Please visit https://tribecode.ai/support for assistance.`);
235
452
  }
453
+
454
+ return true;
236
455
  } catch (error) {
237
456
  spinner.fail(`TRIBE CLI installation failed: ${error.message}`);
457
+
458
+ // Show helpful support information
459
+ console.log('');
460
+ console.log(chalk.yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
461
+ console.log(chalk.yellow('Need help? Visit:'));
462
+ console.log(chalk.cyan.bold(' https://tribecode.ai/support'));
463
+ console.log('');
464
+ console.log(chalk.gray('You can also:'));
465
+ console.log(chalk.gray(' • Check system requirements'));
466
+ console.log(chalk.gray(' • View troubleshooting guides'));
467
+ console.log(chalk.gray(' • Contact our support team'));
468
+ console.log(chalk.yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
469
+ console.log('');
470
+
238
471
  return false;
239
472
  }
240
473
  }
241
474
 
242
475
  async function startContainerRuntime() {
243
- if (platform !== 'darwin') {
244
- return true; // Linux uses Docker daemon directly
245
- }
476
+ const spinner = ora('Starting container runtime...').start();
246
477
 
247
- // Check if container runtime is already working
248
478
  try {
249
- execSync('docker info', { stdio: 'ignore' });
250
- log.success('Container runtime is already working');
251
- return true;
252
- } catch {
253
- // Try to start Colima with different approaches
254
- if (await checkCommand('colima')) {
255
- const spinner = ora('Starting Colima container runtime...').start();
256
-
479
+ // Platform-specific container runtime handling
480
+ if (platform === 'darwin') {
481
+ // macOS - Check if Colima is running
257
482
  try {
258
- // Strategy 1: Quick start with minimal resources and Kubernetes
259
- spinner.text = 'Starting Colima with Kubernetes...';
260
- const colimaPath = await findCommand('colima') || path.join(binDir, 'colima');
261
- execSync(`${colimaPath} start --cpu 2 --memory 4 --disk 10 --kubernetes --vm-type=vz`, {
262
- stdio: 'pipe',
263
- timeout: 60000 // 60 second timeout for K8s
264
- });
265
-
266
- // Test if it worked
267
- execSync('docker info', { stdio: 'ignore' });
268
- spinner.succeed('Colima started successfully');
269
- return true;
483
+ const colimaStatus = execSync('colima status 2>&1', { encoding: 'utf8' });
484
+ if (colimaStatus.includes('is running')) {
485
+ spinner.succeed('Colima is already running');
486
+ return true;
487
+ }
488
+ } catch {
489
+ // Colima not running, need to start it
490
+ }
491
+
492
+ // Try to start Colima
493
+ if (await checkCommand('colima')) {
494
+ const spinner = ora('Starting Colima container runtime...').start();
270
495
 
271
- } catch (error) {
272
- // Strategy 2: Start in background with Kubernetes
273
496
  try {
274
- spinner.text = 'Starting Colima with Kubernetes in background...';
497
+ // Strategy 1: Quick start with minimal resources and Kubernetes
498
+ spinner.text = 'Starting Colima with Kubernetes...';
275
499
  const colimaPath = await findCommand('colima') || path.join(binDir, 'colima');
276
- const child = spawn(colimaPath, ['start', '--cpu', '2', '--memory', '4', '--disk', '10', '--kubernetes'], {
277
- detached: true,
278
- stdio: 'ignore',
279
- env: { ...process.env, PATH: `${binDir}:${process.env.PATH}` }
500
+ execSync(`${colimaPath} start --cpu 2 --memory 4 --disk 10 --kubernetes --vm-type=vz`, {
501
+ stdio: 'pipe',
502
+ timeout: 60000 // 60 second timeout for K8s
280
503
  });
281
- child.unref(); // Don't wait for completion
282
504
 
283
- spinner.succeed('Colima startup initiated (background)');
284
- log.info('Colima is starting in the background');
285
- log.info('Run "colima status" to check progress');
286
- log.info('Run "docker info" to test when ready');
505
+ spinner.succeed('Colima started successfully');
287
506
  return true;
288
507
 
289
- } catch (bgError) {
290
- spinner.fail('Failed to start Colima');
291
- log.warning('Container runtime startup failed (likely due to macOS system restrictions)');
292
- log.info('Options to fix:');
293
- log.info('');
294
- log.info('Option 1 - Manual Colima start:');
295
- log.info(' colima start --cpu 2 --memory 4 # Start container runtime');
296
- log.info(' # This downloads a 344MB disk image (may take time)');
297
- log.info('');
298
- log.info('Option 2 - Use Docker Desktop (easier):');
299
- log.info(' Download from: https://docs.docker.com/desktop/install/mac-install/');
300
- log.info(' # Docker Desktop handles all container runtime setup');
301
- log.info('');
302
- log.info('Option 3 - Use Homebrew (recommended):');
303
- log.info(' brew install colima docker');
304
- log.info(' colima start');
305
- log.info('');
306
- return false;
508
+ } catch (error) {
509
+ // Strategy 2: Start in background with Kubernetes
510
+ try {
511
+ spinner.text = 'Starting Colima with Kubernetes in background...';
512
+ const colimaPath = await findCommand('colima') || path.join(binDir, 'colima');
513
+ const child = spawn(colimaPath, ['start', '--cpu', '2', '--memory', '4', '--disk', '10', '--kubernetes'], {
514
+ detached: true,
515
+ stdio: 'ignore',
516
+ env: { ...process.env, PATH: `${binDir}:${process.env.PATH}` }
517
+ });
518
+ child.unref(); // Don't wait for completion
519
+
520
+ spinner.succeed('Colima startup initiated (background)');
521
+ log.info('Colima is starting in the background');
522
+ log.info('Run "colima status" to check progress');
523
+ return true;
524
+
525
+ } catch (bgError) {
526
+ spinner.fail('Failed to start Colima');
527
+ log.warning('Container runtime startup failed (likely due to macOS system restrictions)');
528
+ log.info('Options to fix:');
529
+ log.info('');
530
+ log.info('Option 1 - Manual Colima start:');
531
+ log.info(' colima start --cpu 2 --memory 4 --kubernetes');
532
+ log.info(' # This downloads a 344MB disk image (may take time)');
533
+ log.info('');
534
+ log.info('Option 2 - Use Homebrew (recommended):');
535
+ log.info(' brew install colima');
536
+ log.info(' colima start --kubernetes');
537
+ log.info('');
538
+ return false;
539
+ }
307
540
  }
308
541
  }
542
+ } else if (platform === 'linux') {
543
+ // Linux - K3s should already be running
544
+ spinner.text = 'Checking K3s status...';
545
+
546
+ // Check if we're in a container (no systemd)
547
+ const isContainer = process.env.container === 'docker' || fs.existsSync('/.dockerenv') || !fs.existsSync('/run/systemd/system');
548
+
549
+ if (isContainer) {
550
+ spinner.info('Running in container - K3s requires systemd');
551
+ log.info('For containers, use KIND or connect to external cluster');
552
+ return true; // Don't fail
553
+ }
554
+
555
+ try {
556
+ execSync('sudo systemctl is-active --quiet k3s', { stdio: 'ignore' });
557
+ spinner.succeed('K3s is running');
558
+ return true;
559
+ } catch {
560
+ // Try to start K3s
561
+ spinner.text = 'Starting K3s...';
562
+ try {
563
+ execSync('sudo systemctl start k3s', { stdio: 'ignore' });
564
+ await new Promise(resolve => setTimeout(resolve, 5000));
565
+ spinner.succeed('K3s started');
566
+ return true;
567
+ } catch (error) {
568
+ spinner.warn('K3s not available - requires systemd');
569
+ log.info('K3s requires systemd. Install K3s manually or use KIND/external cluster');
570
+ return true; // Don't fail
571
+ }
572
+ }
573
+ } else if (platform === 'win32') {
574
+ spinner.fail('Windows support coming soon!');
575
+ log.info('For Windows, please use WSL2 with Ubuntu and run this installer inside WSL2');
576
+ return false;
577
+ } else {
578
+ spinner.fail(`Unsupported platform: ${platform}`);
579
+ return false;
309
580
  }
581
+ } catch (error) {
582
+ spinner.fail(`Container runtime error: ${error.message}`);
583
+ return false;
310
584
  }
311
- return false;
312
585
  }
313
586
 
314
587
  async function updatePath() {
@@ -332,10 +605,98 @@ async function updatePath() {
332
605
  process.env.PATH = `${binDir}:${process.env.PATH}`;
333
606
  }
334
607
 
608
+ async function updateShellConfig() {
609
+ const homeDir = os.homedir();
610
+ const pathExport = `export PATH="$HOME/bin:$PATH"`;
611
+
612
+ // Get current shell
613
+ const currentShell = process.env.SHELL || '/bin/sh';
614
+ const shellName = path.basename(currentShell);
615
+
616
+ // Platform-specific shell config files
617
+ const shellConfigs = {
618
+ 'zsh': ['.zshrc'],
619
+ 'bash': ['.bashrc', '.bash_profile'],
620
+ 'sh': ['.profile'],
621
+ 'fish': ['.config/fish/config.fish'],
622
+ 'ksh': ['.kshrc'],
623
+ 'tcsh': ['.tcshrc', '.cshrc']
624
+ };
625
+
626
+ // On Linux, also update .profile for login shells
627
+ if (platform === 'linux') {
628
+ shellConfigs['bash'].push('.profile');
629
+ }
630
+
631
+ // Get config files for current shell
632
+ const configFiles = shellConfigs[shellName] || ['.profile'];
633
+
634
+ let updated = false;
635
+
636
+ for (const configFile of configFiles) {
637
+ const configPath = path.join(homeDir, configFile);
638
+
639
+ try {
640
+ // Create config file if it doesn't exist
641
+ if (!fs.existsSync(configPath)) {
642
+ // Handle fish shell special syntax
643
+ if (configFile.includes('fish')) {
644
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
645
+ fs.writeFileSync(configPath, `# Created by TRIBE installer\nset -gx PATH $HOME/bin $PATH\n`);
646
+ } else {
647
+ fs.writeFileSync(configPath, `# Created by TRIBE installer\n${pathExport}\n`);
648
+ }
649
+ updated = true;
650
+ continue;
651
+ }
652
+
653
+ // Check if PATH is already configured
654
+ const content = fs.readFileSync(configPath, 'utf8');
655
+ if (content.includes('$HOME/bin') || content.includes('~/bin')) {
656
+ continue; // Already configured
657
+ }
658
+
659
+ // Add PATH export
660
+ if (configFile.includes('fish')) {
661
+ fs.appendFileSync(configPath, `\n# Added by TRIBE installer\nset -gx PATH $HOME/bin $PATH\n`);
662
+ } else {
663
+ fs.appendFileSync(configPath, `\n# Added by TRIBE installer\n${pathExport}\n`);
664
+ }
665
+ updated = true;
666
+ } catch (error) {
667
+ log.warning(`Could not update ${configFile}: ${error.message}`);
668
+ }
669
+ }
670
+
671
+ if (updated) {
672
+ log.success(`Updated shell configuration`);
673
+
674
+ // Linux-specific: also update /etc/environment if we have permission
675
+ if (platform === 'linux' && process.getuid && process.getuid() === 0) {
676
+ try {
677
+ const envPath = '/etc/environment';
678
+ if (fs.existsSync(envPath)) {
679
+ const envContent = fs.readFileSync(envPath, 'utf8');
680
+ if (!envContent.includes('/usr/local/bin')) {
681
+ // Update system PATH
682
+ const newPath = envContent.replace(/PATH="([^"]*)"/, 'PATH="/usr/local/bin:$1"');
683
+ fs.writeFileSync(envPath, newPath);
684
+ log.success('Updated system PATH in /etc/environment');
685
+ }
686
+ }
687
+ } catch (error) {
688
+ // Ignore permission errors
689
+ }
690
+ }
691
+
692
+ log.info(`Run "source ~/${configFiles[0]}" or restart your terminal`);
693
+ }
694
+ }
695
+
335
696
  async function verifyInstallation() {
336
697
  const spinner = ora('Verifying installation...').start();
337
698
 
338
- const tools = ['docker', 'kubectl', 'tribe', 'colima'];
699
+ const tools = ['kubectl', 'tribe', 'colima'];
339
700
  const results = {};
340
701
 
341
702
  for (const tool of tools) {
@@ -344,11 +705,27 @@ async function verifyInstallation() {
344
705
 
345
706
  // Special check for container runtime
346
707
  let containerWorking = false;
347
- try {
348
- execSync('docker info', { stdio: 'ignore' });
349
- containerWorking = true;
350
- } catch (error) {
351
- containerWorking = false;
708
+ if (platform === 'darwin') {
709
+ try {
710
+ const colimaStatus = execSync('colima status 2>&1', { encoding: 'utf8' });
711
+ containerWorking = colimaStatus.includes('is running');
712
+ } catch {
713
+ containerWorking = false;
714
+ }
715
+ } else if (platform === 'linux') {
716
+ try {
717
+ // Check if k3s is running
718
+ execSync('systemctl is-active k3s', { stdio: 'ignore' });
719
+ containerWorking = true;
720
+ } catch {
721
+ // Fallback to kubectl
722
+ try {
723
+ execSync('kubectl cluster-info', { stdio: 'ignore' });
724
+ containerWorking = true;
725
+ } catch {
726
+ containerWorking = false;
727
+ }
728
+ }
352
729
  }
353
730
 
354
731
  spinner.stop();
@@ -412,7 +789,14 @@ async function startColimaWithKubernetes() {
412
789
  // Check if it has Kubernetes enabled
413
790
  if (await checkColimaHasKubernetes()) {
414
791
  spinner.succeed('Colima is already running with Kubernetes');
415
- return true;
792
+
793
+ // Verify kubectl works
794
+ try {
795
+ execSync('kubectl cluster-info', { stdio: 'ignore' });
796
+ return true;
797
+ } catch {
798
+ spinner.info('Colima is running but kubectl not connected, continuing...');
799
+ }
416
800
  } else {
417
801
  spinner.text = 'Colima is running without Kubernetes. Restarting with Kubernetes...';
418
802
 
@@ -488,6 +872,32 @@ async function deployTribeCluster() {
488
872
  if (fs.existsSync(sourceYaml)) {
489
873
  fs.copyFileSync(sourceYaml, destYaml);
490
874
  log.info('Copied deployment configuration');
875
+ } else {
876
+ // Download deployment YAML if not bundled
877
+ spinner.text = 'Downloading deployment configuration...';
878
+ const yamlSources = [
879
+ // Direct from GitHub (primary)
880
+ 'https://raw.githubusercontent.com/TRIBE-INC/0zen/main/sdk/cmd/cli-gui/package/tribe-deployment.yaml',
881
+ // jsDelivr CDN as fallback
882
+ 'https://cdn.jsdelivr.net/gh/TRIBE-INC/0zen@main/sdk/cmd/cli-gui/package/tribe-deployment.yaml'
883
+ ];
884
+
885
+ let yamlDownloaded = false;
886
+ for (const yamlUrl of yamlSources) {
887
+ try {
888
+ await downloadFile(yamlUrl, destYaml);
889
+ log.info('Downloaded deployment configuration');
890
+ yamlDownloaded = true;
891
+ break;
892
+ } catch (downloadError) {
893
+ // Try next source
894
+ }
895
+ }
896
+
897
+ if (!yamlDownloaded) {
898
+ spinner.fail('Failed to get deployment configuration');
899
+ throw new Error('Could not download deployment YAML from any source');
900
+ }
491
901
  }
492
902
 
493
903
  // Run tribe start command with deployment YAML path
@@ -626,19 +1036,26 @@ async function deployTribeCluster() {
626
1036
  log.info(' colima start --kubernetes');
627
1037
  log.info('3. Then run: tribe start');
628
1038
  } else {
629
- log.info('You can manually deploy later with: tribe start');
1039
+ log.info('You can manually deploy later with: tribe start');
630
1040
  }
631
1041
  return false;
632
1042
  }
633
1043
  }
634
1044
 
635
1045
  async function promptForClusterSetup() {
1046
+ // Check for auto-approve in CI environments
1047
+ if (process.env.CI === 'true' || process.env.TRIBE_AUTO_APPROVE === 'true') {
1048
+ console.log('\n' + chalk.bold('🚀 TRIBE Cluster Setup'));
1049
+ console.log(chalk.green('Auto-approving cluster setup (CI mode)'));
1050
+ return true;
1051
+ }
1052
+
636
1053
  // Simple prompt without external dependencies
637
1054
  return new Promise((resolve) => {
638
1055
  console.log('\n' + chalk.bold('🚀 TRIBE Cluster Setup'));
639
1056
  console.log('\nWould you like to set up the TRIBE cluster now?');
640
1057
  console.log('This will:');
641
- console.log(' • Start Colima with Kubernetes');
1058
+ console.log(' • Start ' + (platform === 'darwin' ? 'Colima' : 'K3s') + ' with Kubernetes');
642
1059
  console.log(' • Deploy all TRIBE services');
643
1060
  console.log(' • Set up port forwarding');
644
1061
  console.log('\n' + chalk.yellow('Note: This requires ~2GB disk space and may take a few minutes'));
@@ -655,20 +1072,124 @@ async function promptForClusterSetup() {
655
1072
  });
656
1073
  }
657
1074
 
1075
+ async function cleanupOldInstallations() {
1076
+ // Clean up old aliases
1077
+ const rcFiles = ['.zshrc', '.bashrc'];
1078
+ rcFiles.forEach(file => {
1079
+ const rcPath = path.join(homeDir, file);
1080
+ if (fs.existsSync(rcPath)) {
1081
+ try {
1082
+ let content = fs.readFileSync(rcPath, 'utf8');
1083
+ const originalContent = content;
1084
+
1085
+ // Remove old tribe-cli aliases
1086
+ content = content.replace(/^alias tribe=['"]tribe-cli['"]\s*$/gm, '# $& # Removed by TRIBE installer');
1087
+ content = content.replace(/^alias t=['"]tribe-cli['"]\s*$/gm, '# $& # Removed by TRIBE installer');
1088
+
1089
+ if (content !== originalContent) {
1090
+ fs.writeFileSync(rcPath, content);
1091
+ log.info(`Cleaned up old aliases in ${file}`);
1092
+ }
1093
+ } catch (error) {
1094
+ log.warning(`Could not clean up ${file}: ${error.message}`);
1095
+ }
1096
+ }
1097
+ });
1098
+ }
1099
+
658
1100
  async function main() {
659
1101
  console.log(chalk.bold.blue('\n🚀 TRIBE CLI Complete Installer\n'));
660
1102
 
1103
+ // Detect and display platform info
1104
+ const displayArch = process.arch === 'x64' ? 'amd64' : process.arch;
1105
+ console.log(chalk.cyan(`Platform: ${platform} (${displayArch})`));
1106
+ console.log(chalk.cyan(`Node: ${process.version}`));
1107
+ console.log('');
1108
+
1109
+ // Check for unsupported platforms early
1110
+ if (platform === 'win32') {
1111
+ console.log(chalk.yellow('\n⚠️ Windows is not yet supported'));
1112
+ console.log(chalk.yellow('Please use WSL2 with Ubuntu and run this installer inside WSL2'));
1113
+ console.log(chalk.yellow('Instructions: https://docs.microsoft.com/en-us/windows/wsl/install\n'));
1114
+ process.exit(1);
1115
+ }
1116
+
1117
+ // Edge case detection
1118
+ // 1. Check for unsupported architectures
1119
+ const supportedArchitectures = ['x64', 'x86_64', 'arm64', 'aarch64'];
1120
+ if (!supportedArchitectures.includes(nodeArch)) {
1121
+ console.log(chalk.red('\n❌ Unsupported Architecture'));
1122
+ console.log(chalk.red(`Your system architecture (${nodeArch}) is not supported.`));
1123
+ console.log(chalk.yellow('TRIBE only provides binaries for x64 (AMD64) and ARM64.\n'));
1124
+ process.exit(1);
1125
+ }
1126
+
1127
+ // 2. WSL2 detection
1128
+ if (fs.existsSync('/proc/sys/fs/binfmt_misc/WSLInterop') || process.env.WSL_DISTRO_NAME) {
1129
+ console.log(chalk.yellow('\n⚠️ WSL2 Environment Detected'));
1130
+ console.log(chalk.yellow('K3s may have networking limitations in WSL2.'));
1131
+ console.log(chalk.yellow('Consider using Docker Desktop with WSL2 backend instead.\n'));
1132
+ // Don't exit, just warn
1133
+ }
1134
+
1135
+ // 3. Proxy detection
1136
+ const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
1137
+ if (proxyUrl) {
1138
+ console.log(chalk.blue('ℹ️ Proxy detected: ' + proxyUrl));
1139
+ console.log(chalk.blue('Downloads will use system proxy settings.\n'));
1140
+ }
1141
+
1142
+ // 4. Check for existing Kubernetes installations
1143
+ const k8sTools = [];
1144
+ const checkTools = ['minikube', 'microk8s', 'kind', 'k3d', 'kubectl'];
1145
+ for (const tool of checkTools) {
1146
+ try {
1147
+ execSync(`which ${tool}`, { stdio: 'ignore' });
1148
+ k8sTools.push(tool);
1149
+ } catch {}
1150
+ }
1151
+
1152
+ if (k8sTools.length > 0) {
1153
+ console.log(chalk.blue('\n📦 Existing Kubernetes tools detected: ' + k8sTools.join(', ')));
1154
+ console.log(chalk.blue('You can use --skip-cluster to skip K3s installation.\n'));
1155
+ }
1156
+
1157
+ // 5. Disk space check
1158
+ try {
1159
+ const stats = fs.statfsSync(homeDir);
1160
+ const freeGB = (stats.bavail * stats.bsize) / (1024 ** 3);
1161
+ if (freeGB < 3) {
1162
+ console.log(chalk.yellow(`\n⚠️ Low disk space: ${freeGB.toFixed(1)}GB free`));
1163
+ console.log(chalk.yellow('K3s installation requires at least 3GB of free space.\n'));
1164
+ }
1165
+ } catch {}
1166
+
1167
+ // Clean up old installations
1168
+ await cleanupOldInstallations();
1169
+
661
1170
  log.info(`Detected: ${platform} (${arch})`);
662
1171
  log.info(`Installing to: ${binDir}`);
663
1172
 
664
1173
  // Update PATH first
665
1174
  await updatePath();
666
1175
 
667
- const tasks = [
668
- { name: 'Colima', fn: installColima },
1176
+ // Update shell configuration files
1177
+ await updateShellConfig();
1178
+
1179
+ const tasks = [];
1180
+
1181
+ // Platform-specific Kubernetes installation
1182
+ if (platform === 'darwin') {
1183
+ tasks.push({ name: 'Colima', fn: installColima });
1184
+ } else if (platform === 'linux') {
1185
+ tasks.push({ name: 'K3s', fn: installK3s });
1186
+ }
1187
+
1188
+ // Common tools for all platforms
1189
+ tasks.push(
669
1190
  { name: 'kubectl', fn: installKubectl },
670
- { name: 'TRIBE CLI', fn: installTribeCli }
671
- ];
1191
+ { name: 'TRIBE CLI', fn: installTribeCLI }
1192
+ );
672
1193
 
673
1194
  let allSuccess = true;
674
1195
 
@@ -688,7 +1209,7 @@ async function main() {
688
1209
  // Check if cluster already exists
689
1210
  const clusterExists = await checkClusterExists();
690
1211
 
691
- if (!clusterExists) {
1212
+ if (!clusterExists && !global.skipCluster) {
692
1213
  // Prompt for cluster setup
693
1214
  const shouldSetup = await promptForClusterSetup();
694
1215
 
@@ -764,17 +1285,30 @@ async function main() {
764
1285
  console.log('');
765
1286
  // Check container runtime for guidance
766
1287
  let runtimeWorking = false;
767
- try {
768
- execSync('docker info', { stdio: 'ignore' });
769
- runtimeWorking = true;
770
- } catch {
771
- runtimeWorking = false;
772
- }
773
-
774
- if (!runtimeWorking) {
775
- log.info('Start container runtime:');
776
- console.log(' colima start # macOS');
777
- console.log(' sudo systemctl start docker # Linux');
1288
+ if (platform === 'darwin') {
1289
+ try {
1290
+ const colimaStatus = execSync('colima status 2>&1', { encoding: 'utf8' });
1291
+ runtimeWorking = colimaStatus.includes('is running');
1292
+ } catch {
1293
+ runtimeWorking = false;
1294
+ }
1295
+
1296
+ if (!runtimeWorking) {
1297
+ log.info('Start container runtime:');
1298
+ console.log(' colima start --kubernetes # Start Colima with Kubernetes');
1299
+ }
1300
+ } else if (platform === 'linux') {
1301
+ try {
1302
+ execSync('systemctl is-active k3s', { stdio: 'ignore' });
1303
+ runtimeWorking = true;
1304
+ } catch {
1305
+ runtimeWorking = false;
1306
+ }
1307
+
1308
+ if (!runtimeWorking) {
1309
+ log.info('Start container runtime:');
1310
+ console.log(' sudo systemctl start k3s # Start k3s');
1311
+ }
778
1312
  }
779
1313
  console.log('');
780
1314
  log.info('Restart your shell or run: source ~/.zshrc');
@@ -792,13 +1326,24 @@ if (require.main === module) {
792
1326
 
793
1327
  if (args.includes('--help') || args.includes('-h')) {
794
1328
  console.log(chalk.bold.blue('TRIBE CLI Installer\n'));
795
- console.log('Usage: npx tribe-cli-local [options]\n');
1329
+ console.log('Usage: npx @_xtribe/cli [options]\n');
796
1330
  console.log('Options:');
797
1331
  console.log(' --help, -h Show this help message');
798
1332
  console.log(' --verify Only verify existing installation');
799
1333
  console.log(' --dry-run Show what would be installed');
800
- console.log('\nThis installer sets up the complete TRIBE development environment:');
801
- console.log(' Colima (Container runtime with Kubernetes)');
1334
+ console.log(' --skip-cluster Skip cluster deployment (for testing)');
1335
+ console.log('\nThis installer sets up the complete TRIBE development environment:');
1336
+
1337
+ if (platform === 'darwin') {
1338
+ console.log('• Colima (Container runtime with Kubernetes)');
1339
+ } else if (platform === 'linux') {
1340
+ console.log('• K3s (Lightweight Kubernetes, one-line install)');
1341
+ } else if (platform === 'win32') {
1342
+ console.log('\n⚠️ Windows support coming soon!');
1343
+ console.log('Please use WSL2 with Ubuntu and run this installer inside WSL2');
1344
+ process.exit(1);
1345
+ }
1346
+
802
1347
  console.log('• kubectl (Kubernetes CLI)');
803
1348
  console.log('• TRIBE CLI (Multi-agent orchestration)');
804
1349
  console.log('\nAfter installation:');
@@ -826,6 +1371,9 @@ if (require.main === module) {
826
1371
  process.exit(0);
827
1372
  }
828
1373
 
1374
+ // Store skip-cluster flag
1375
+ global.skipCluster = args.includes('--skip-cluster');
1376
+
829
1377
  main().catch(error => {
830
1378
  console.error(chalk.red('Installation failed:'), error.message);
831
1379
  process.exit(1);