@_xtribe/cli 2.0.5 → 2.0.7

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 +290 -1751
  2. package/package.json +1 -1
package/install-tribe.js CHANGED
@@ -10,26 +10,38 @@ const chalk = require('chalk');
10
10
  const ora = require('ora');
11
11
  const which = require('which');
12
12
  const fetch = require('node-fetch');
13
- const crypto = require('crypto'); // Added for checksum verification
13
+ const crypto = require('crypto');
14
+
15
+ // ASCII art for TRIBE
16
+ const asciiArt = `
17
+ ╔══════════════════════════════════════════════════════════════════╗
18
+ ║ ║
19
+ ║ ████████╗██████╗ ██╗██████╗ ███████╗ ║
20
+ ║ ╚══██╔══╝██╔══██╗██║██╔══██╗██╔════╝ ║
21
+ ║ ██║ ██████╔╝██║██████╔╝█████╗ ║
22
+ ║ ██║ ██╔══██╗██║██╔══██╗██╔══╝ ║
23
+ ║ ██║ ██║ ██║██║██████╔╝███████╗ ║
24
+ ║ ╚═╝ ╚═╝ ╚═╝╚═╝╚═════╝ ╚══════╝ ║
25
+ ║ ║
26
+ ║ AI Development Teams That Make You 10x Faster ║
27
+ ║ ║
28
+ ╚══════════════════════════════════════════════════════════════════╝`;
29
+
30
+ // Track shown warnings to prevent duplicates
31
+ const shownWarnings = new Set();
14
32
 
15
33
  const platform = os.platform();
16
- // Map Node.js arch to standard naming (x64 -> amd64, etc.)
17
34
  const nodeArch = os.arch();
18
35
  const arch = nodeArch === 'x64' ? 'amd64' : (nodeArch === 'arm64' || nodeArch === 'aarch64' ? 'arm64' : nodeArch);
19
36
  const homeDir = os.homedir();
20
37
  const tribeDir = path.join(homeDir, '.tribe');
21
38
  const tribeBinDir = path.join(tribeDir, 'bin');
22
- // Use consistent directory for all binaries
23
39
  const binDir = tribeBinDir;
24
40
 
25
- // No need to create ~/bin anymore since we're using ~/.tribe/bin
26
-
27
- // Ensure TRIBE config directory exists
41
+ // Ensure directories exist
28
42
  if (!fs.existsSync(tribeDir)) {
29
43
  fs.mkdirSync(tribeDir, { recursive: true });
30
44
  }
31
-
32
- // Ensure TRIBE bin directory exists
33
45
  if (!fs.existsSync(tribeBinDir)) {
34
46
  fs.mkdirSync(tribeBinDir, { recursive: true });
35
47
  }
@@ -37,61 +49,94 @@ if (!fs.existsSync(tribeBinDir)) {
37
49
  const log = {
38
50
  success: (msg) => console.log(chalk.green('✓'), msg),
39
51
  error: (msg) => console.log(chalk.red('✗'), msg),
40
- warning: (msg) => console.log(chalk.yellow('⚠'), msg),
52
+ warning: (msg) => {
53
+ // Check if we've already shown this warning
54
+ if (!shownWarnings.has(msg)) {
55
+ console.log(chalk.yellow('⚠'), msg);
56
+ shownWarnings.add(msg);
57
+ }
58
+ },
41
59
  info: (msg) => console.log(chalk.blue('ℹ'), msg),
42
60
  step: (msg) => console.log(chalk.cyan('→'), msg)
43
61
  };
44
62
 
63
+ function showWelcome() {
64
+ console.clear();
65
+ console.log(chalk.cyan(asciiArt));
66
+ console.log(chalk.bold('\n🚀 Welcome to TRIBE\n'));
67
+ console.log('Transform into a 10x developer with AI-powered development teams.\n');
68
+ console.log('TRIBE provides you with specialized AI agents that:');
69
+ console.log(' • Write production-ready code');
70
+ console.log(' • Review and improve your work');
71
+ console.log(' • Handle DevOps and infrastructure');
72
+ console.log(' • Automate testing and quality assurance\n');
73
+
74
+ console.log(chalk.gray(`Platform: ${platform} (${arch})`));
75
+ console.log(chalk.gray(`Node: ${process.version}\n`));
76
+ }
77
+
78
+ function showInstallationSummary() {
79
+ console.log('\n' + '═'.repeat(70));
80
+ console.log(chalk.green.bold('\n✨ TRIBE Installation Complete!\n'));
81
+
82
+ console.log('📋 Installation Summary:');
83
+ console.log(' ✓ Runtime: ' + (platform === 'darwin' ? 'Colima' : 'Native') + ' with Kubernetes');
84
+ console.log(' ✓ CLI: TRIBE command available globally');
85
+ console.log(' ✓ Config: ~/.tribe directory initialized\n');
86
+
87
+ console.log(chalk.bold('🎯 Your AI Development Team:'));
88
+ const agents = [
89
+ { emoji: '🎨', name: 'Pixel', role: 'Creative Design & UI/UX' },
90
+ { emoji: '⚡', name: 'Spark', role: 'Implementation & Performance' },
91
+ { emoji: '🌟', name: 'Nova', role: 'Architecture & Strategy' },
92
+ { emoji: '🧘', name: 'Zen', role: 'Testing & Quality' },
93
+ { emoji: '🔥', name: 'Blaze', role: 'DevOps & Infrastructure' },
94
+ { emoji: '🌌', name: 'Cosmos', role: 'Data & Analytics' },
95
+ { emoji: '🔊', name: 'Echo', role: 'API & Integration' },
96
+ { emoji: '⚛️', name: 'Quantum', role: 'Security & Cryptography' }
97
+ ];
98
+
99
+ agents.forEach(agent => {
100
+ console.log(` ${agent.emoji} ${chalk.cyan(agent.name)} - ${agent.role}`);
101
+ });
102
+
103
+ console.log('\n' + chalk.bold('🚀 Quick Start:'));
104
+ console.log(chalk.cyan(' tribe login') + ' # Authenticate with TRIBE');
105
+ console.log(chalk.cyan(' tribe enable') + ' # Start telemetry');
106
+ console.log(chalk.cyan(' tribe status') + ' # Check your setup\n');
107
+
108
+ console.log(chalk.gray('Documentation: https://tribecode.ai/docs'));
109
+ console.log(chalk.gray('Support: https://tribecode.ai/support'));
110
+ console.log('\n' + '═'.repeat(70) + '\n');
111
+ }
112
+
45
113
  async function checkCommand(cmd) {
46
114
  try {
47
115
  await which(cmd);
48
116
  return true;
49
117
  } 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
- }
118
+ const cmdPath = path.join(tribeBinDir, cmd);
119
+ try {
120
+ await fs.promises.access(cmdPath, fs.constants.X_OK);
121
+ return true;
122
+ } catch {
123
+ return false;
75
124
  }
76
- return false;
77
125
  }
78
126
  }
79
127
 
80
128
  async function findCommand(cmd) {
81
- // Try to find command in various locations
82
129
  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
130
+ path.join(binDir, cmd),
131
+ path.join('/opt/homebrew/bin', cmd),
132
+ path.join('/usr/local/bin', cmd),
133
+ cmd
87
134
  ];
88
135
 
89
- // First try 'which' command
90
136
  try {
91
137
  const cmdPath = await which(cmd);
92
138
  return cmdPath;
93
139
  } catch {
94
- // If not in PATH, check known locations
95
140
  for (const cmdPath of possiblePaths) {
96
141
  try {
97
142
  await fs.promises.access(cmdPath, fs.constants.X_OK);
@@ -110,7 +155,6 @@ async function downloadFile(url, dest) {
110
155
  const file = fs.createWriteStream(dest);
111
156
  https.get(url, (response) => {
112
157
  if (response.statusCode === 302 || response.statusCode === 301) {
113
- // Handle redirects
114
158
  return downloadFile(response.headers.location, dest).then(resolve, reject);
115
159
  }
116
160
  if (response.statusCode !== 200) {
@@ -121,31 +165,24 @@ async function downloadFile(url, dest) {
121
165
  file.on('finish', async () => {
122
166
  file.close();
123
167
 
124
- // Check if this might be a pointer file
168
+ // Check for pointer files
125
169
  const stats = fs.statSync(dest);
126
- if (stats.size < 500) { // Increased size check - binaries are much larger
170
+ if (stats.size < 500) {
127
171
  const content = fs.readFileSync(dest, 'utf8').trim();
128
- // Check if content looks like a path (e.g., "main-121/tribe-linux-amd64")
129
172
  if (content.match(/^[\w\-\/]+$/) && !content.includes('<!DOCTYPE') && !content.includes('<html')) {
130
- console.log(`📎 Detected pointer file, following to: ${content}`);
131
173
  let actualBinaryUrl;
132
174
 
133
- // Handle different pointer formats
134
175
  if (content.startsWith('http')) {
135
- // Direct URL
136
176
  actualBinaryUrl = content;
137
177
  } else if (content.includes('/')) {
138
- // Relative path - construct full URL
139
178
  const baseUrl = url.substring(0, url.lastIndexOf('/'));
140
179
  actualBinaryUrl = `${baseUrl}/${content}`;
141
180
  } else {
142
- // Just a filename
143
181
  const baseUrl = url.substring(0, url.lastIndexOf('/'));
144
182
  actualBinaryUrl = `${baseUrl}/${content}`;
145
183
  }
146
184
 
147
- // Download the actual binary
148
- fs.unlinkSync(dest); // Remove pointer file
185
+ fs.unlinkSync(dest);
149
186
  await downloadFile(actualBinaryUrl, dest);
150
187
  }
151
188
  }
@@ -156,139 +193,41 @@ async function downloadFile(url, dest) {
156
193
  });
157
194
  }
158
195
 
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
196
  async function installColima() {
252
197
  if (platform !== 'darwin') {
253
- log.info('Colima is only needed on macOS - skipping');
254
198
  return true;
255
199
  }
256
200
 
257
201
  const existingColima = await findCommand('colima');
258
202
  if (existingColima) {
259
- log.success(`Colima already installed at: ${existingColima}`);
203
+ console.log(`🐳 Colima ${chalk.gray('(already installed)')}`);
260
204
  return true;
261
205
  }
262
206
 
263
- const spinner = ora('Installing Colima...').start();
207
+ const spinner = ora('🐳 Installing Colima...').start();
264
208
 
265
209
  try {
266
- // Strategy 1: Try Homebrew if available (better signing)
210
+ // Try Homebrew first
267
211
  try {
268
212
  execSync('brew --version', { stdio: 'ignore' });
269
- spinner.text = 'Installing Colima via Homebrew...';
270
213
  execSync('brew install colima', { stdio: 'ignore' });
271
- spinner.succeed('Colima installed via Homebrew');
214
+ spinner.succeed('Colima installed');
272
215
  return true;
273
- } catch (brewError) {
274
- // Homebrew not available, continue with direct download
275
- spinner.text = 'Installing Colima (direct download)...';
216
+ } catch {
217
+ // Continue with direct download
276
218
  }
277
219
 
278
- // Strategy 2: Direct download with Gatekeeper approval
220
+ // Direct download
279
221
  const colimaUrl = `https://github.com/abiosoft/colima/releases/latest/download/colima-${platform}-${arch}`;
280
222
  const colimaDest = path.join(binDir, 'colima');
281
223
 
282
224
  await downloadFile(colimaUrl, colimaDest);
283
225
  fs.chmodSync(colimaDest, '755');
284
226
 
285
- // Try to remove quarantine attribute (macOS Sequoia workaround)
286
227
  try {
287
228
  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)');
229
+ } catch {
230
+ // Quarantine attribute may not exist
292
231
  }
293
232
 
294
233
  spinner.succeed('Colima installed');
@@ -299,36 +238,15 @@ async function installColima() {
299
238
  }
300
239
  }
301
240
 
302
- // Lima installation removed - Colima handles virtualization
303
-
304
-
305
241
  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
242
  if (await checkCommand('kubectl')) {
324
- log.success('kubectl already installed');
243
+ console.log(`☸️ kubectl ${chalk.gray('(already installed)')}`);
325
244
  return true;
326
245
  }
327
246
 
328
- const spinner = ora('Installing kubectl...').start();
247
+ const spinner = ora('☸️ Installing kubectl...').start();
329
248
 
330
249
  try {
331
- // Get latest stable version
332
250
  const versionResponse = await fetch('https://dl.k8s.io/release/stable.txt');
333
251
  const version = await versionResponse.text();
334
252
  const kubectlUrl = `https://dl.k8s.io/release/${version.trim()}/bin/${platform}/${arch}/kubectl`;
@@ -345,81 +263,118 @@ async function installKubectl() {
345
263
  }
346
264
  }
347
265
 
348
- async function setupPathEnvironment() {
349
- const spinner = ora('Setting up PATH environment...').start();
266
+ async function installTribeCLI() {
267
+ const tribeDest = path.join(tribeBinDir, 'tribe');
268
+
269
+ // Check if we need to update
270
+ const existingTribe = await checkCommand('tribe');
271
+ const isUpdate = existingTribe;
272
+
273
+ const spinner = ora(isUpdate ? '🚀 Updating TRIBE CLI...' : '🚀 Installing TRIBE CLI...').start();
350
274
 
351
275
  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
- `;
276
+ // Remove existing if present
277
+ if (fs.existsSync(tribeDest)) {
278
+ fs.unlinkSync(tribeDest);
279
+ }
379
280
 
380
- fs.writeFileSync(envScriptDest, envScriptContent);
381
- fs.chmodSync(envScriptDest, '755');
281
+ // Check for local binary first
282
+ const localBinary = path.join(__dirname, 'tribe');
283
+ if (fs.existsSync(localBinary)) {
284
+ fs.copyFileSync(localBinary, tribeDest);
285
+ fs.chmodSync(tribeDest, '755');
286
+ spinner.succeed(isUpdate ? 'TRIBE CLI updated' : 'TRIBE CLI installed');
287
+ return true;
288
+ }
382
289
 
383
- spinner.succeed('PATH environment script created');
290
+ // Download from GitHub
291
+ const githubRepo = process.env.TRIBE_INSTALLER_REPO || 'TRIBE-INC/releases';
292
+ const versionChannel = global.versionChannel || 'latest';
384
293
 
385
- // Store that we created the env script
386
- global.envScriptCreated = true;
294
+ let downloadUrl = '';
387
295
 
388
- // Now automatically run setup-path to configure the shell
389
296
  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');
297
+ const response = await fetch(`https://api.github.com/repos/${githubRepo}/releases`, {
298
+ headers: {
299
+ 'User-Agent': 'TRIBE-Installer',
300
+ ...(process.env.GITHUB_TOKEN ? { 'Authorization': `token ${process.env.GITHUB_TOKEN}` } : {})
301
+ }
302
+ });
303
+
304
+ if (response.ok) {
305
+ const releases = await response.json();
306
+ if (releases && releases.length > 0) {
307
+ const asset = releases[0].assets?.find(a =>
308
+ a.name === `tribe-${platform}-${arch}` ||
309
+ a.name === `tribe-${platform}-${arch}.exe`
310
+ );
311
+ if (asset) {
312
+ downloadUrl = asset.browser_download_url;
313
+ }
314
+ }
315
+ }
316
+ } catch {
317
+ // Fallback to direct URL
318
+ }
319
+
320
+ if (!downloadUrl) {
321
+ downloadUrl = `https://github.com/${githubRepo}/releases/latest/download/tribe-${platform}-${arch}`;
322
+ }
323
+
324
+ await downloadFile(downloadUrl, tribeDest);
325
+ fs.chmodSync(tribeDest, '755');
326
+
327
+ // Remove macOS quarantine
328
+ if (platform === 'darwin') {
329
+ try {
330
+ execSync(`xattr -d com.apple.quarantine "${tribeDest}"`, { stdio: 'ignore' });
331
+ } catch {
332
+ // Not critical
333
+ }
396
334
  }
397
335
 
336
+ spinner.succeed(isUpdate ? 'TRIBE CLI updated' : 'TRIBE CLI installed');
398
337
  return true;
399
338
  } catch (error) {
400
- spinner.fail(`Failed to create PATH environment script: ${error.message}`);
339
+ spinner.fail(`TRIBE CLI installation failed: ${error.message}`);
401
340
  return false;
402
341
  }
403
342
  }
404
343
 
405
- async function setupGlobalNpmCommand() {
406
- const spinner = ora('Setting up global tribe command...').start();
344
+ async function setupPathEnvironment() {
345
+ const envScriptDest = path.join(tribeDir, 'tribe-env.sh');
407
346
 
347
+ const envScriptContent = `#!/bin/bash
348
+ # TRIBE CLI Environment Setup
349
+
350
+ TRIBE_BIN_DIR="$HOME/.tribe/bin"
351
+
352
+ if [[ -d "$TRIBE_BIN_DIR" ]] && [[ ":$PATH:" != *":$TRIBE_BIN_DIR:"* ]]; then
353
+ export PATH="$TRIBE_BIN_DIR:$PATH"
354
+ fi
355
+
356
+ command -v tribe &> /dev/null || true
357
+ `;
358
+
359
+ fs.writeFileSync(envScriptDest, envScriptContent);
360
+ fs.chmodSync(envScriptDest, '755');
361
+
362
+ return true;
363
+ }
364
+
365
+ async function setupGlobalNpmCommand() {
408
366
  try {
409
- // Create a temporary directory for the global package
410
367
  const tempDir = path.join(os.tmpdir(), 'tribe-global-install-' + Date.now());
411
368
  fs.mkdirSync(tempDir, { recursive: true });
412
369
 
413
- // Create package.json for global command
414
370
  const packageJson = {
415
371
  name: 'tribe-cli-global',
416
372
  version: '1.0.0',
417
373
  description: 'TRIBE CLI global command',
418
374
  bin: {
419
- tribe: './tribe-wrapper.js',
420
- 'tribe-logs': './tribe-logs-wrapper.js'
375
+ tribe: './tribe-wrapper.js'
421
376
  },
422
- files: ['tribe-wrapper.js', 'tribe-logs-wrapper.js'],
377
+ files: ['tribe-wrapper.js'],
423
378
  private: true
424
379
  };
425
380
 
@@ -428,44 +383,6 @@ async function setupGlobalNpmCommand() {
428
383
  JSON.stringify(packageJson, null, 2)
429
384
  );
430
385
 
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
386
  const wrapperScript = `#!/usr/bin/env node
470
387
 
471
388
  const { spawn } = require('child_process');
@@ -473,26 +390,22 @@ const fs = require('fs');
473
390
  const path = require('path');
474
391
  const os = require('os');
475
392
 
476
- // Path to the actual tribe binary installed by the installer
477
393
  const tribeBinaryPath = path.join(os.homedir(), '.tribe', 'bin', 'tribe');
478
394
 
479
- // Check if tribe binary exists
480
395
  if (!fs.existsSync(tribeBinaryPath)) {
481
396
  console.error('Error: TRIBE CLI binary not found.');
482
- console.error('Please reinstall using: npx @_xtribe/cli --force');
397
+ console.error('Please reinstall using: npx @_xtribe/cli@latest');
483
398
  process.exit(1);
484
399
  }
485
400
 
486
- // Make sure binary is executable
487
401
  try {
488
402
  fs.accessSync(tribeBinaryPath, fs.constants.X_OK);
489
403
  } catch (err) {
490
404
  console.error('Error: TRIBE CLI binary is not executable.');
491
- console.error('Please reinstall using: npx @_xtribe/cli --force');
405
+ console.error('Please reinstall using: npx @_xtribe/cli@latest');
492
406
  process.exit(1);
493
407
  }
494
408
 
495
- // Forward all arguments to the actual tribe binary
496
409
  const child = spawn(tribeBinaryPath, process.argv.slice(2), {
497
410
  stdio: 'inherit',
498
411
  env: process.env
@@ -505,7 +418,7 @@ child.on('exit', (code) => {
505
418
  child.on('error', (err) => {
506
419
  if (err.code === 'ENOENT') {
507
420
  console.error('Error: TRIBE CLI binary not found at expected location.');
508
- console.error('Please reinstall using: npx @_xtribe/cli --force');
421
+ console.error('Please reinstall using: npx @_xtribe/cli@latest');
509
422
  } else {
510
423
  console.error('Failed to execute tribe:', err.message);
511
424
  }
@@ -516,1515 +429,141 @@ child.on('error', (err) => {
516
429
  fs.writeFileSync(wrapperPath, wrapperScript);
517
430
  fs.chmodSync(wrapperPath, '755');
518
431
 
519
- // Install globally using npm
520
- spinner.text = 'Installing tribe command globally...';
521
432
  try {
522
- // First try with --force to handle conflicts
523
433
  execSync('npm install -g . --force', {
524
434
  cwd: tempDir,
525
435
  stdio: 'pipe'
526
436
  });
527
- spinner.succeed('Global tribe command installed successfully');
528
437
  } catch (error) {
529
- // Try with sudo if permission denied
530
438
  if (error.message.includes('EACCES') || error.message.includes('permission')) {
531
- spinner.warn('Global installation requires sudo permission');
532
- log.info('Attempting with sudo...');
533
439
  try {
534
440
  execSync('sudo npm install -g . --force', {
535
441
  cwd: tempDir,
536
442
  stdio: 'inherit'
537
443
  });
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');
444
+ } catch {
445
+ // Ignore errors
554
446
  }
555
- } else {
556
- throw error;
557
447
  }
558
448
  }
559
449
 
560
- // Clean up temp directory
561
450
  try {
562
451
  fs.rmSync(tempDir, { recursive: true, force: true });
563
- } catch (cleanupError) {
452
+ } catch {
564
453
  // Ignore cleanup errors
565
454
  }
566
455
 
567
456
  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');
457
+ } catch {
571
458
  return false;
572
459
  }
573
460
  }
574
461
 
575
- async function installTribeLogViewer() {
462
+ async function startColimaWithKubernetes() {
463
+ const spinner = ora('🌟 Starting container runtime...').start();
464
+
576
465
  try {
577
- // Install the log viewer script
578
- const logViewerSource = path.join(__dirname, 'tribe-logs.sh');
579
- const logViewerDest = path.join(tribeBinDir, 'tribe-logs');
466
+ const colimaPath = await findCommand('colima') || path.join(binDir, 'colima');
580
467
 
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');
468
+ // Check if running
469
+ try {
470
+ const status = execSync(`${colimaPath} status 2>&1`, { encoding: 'utf8' });
471
+ if (status.includes('is running')) {
472
+ spinner.succeed('Container runtime ready');
473
+ return true;
474
+ }
475
+ } catch {
476
+ // Not running, start it
585
477
  }
478
+
479
+ spinner.text = 'Starting Colima with Kubernetes...';
480
+ execSync(`${colimaPath} start --kubernetes --cpu 4 --memory 8 --disk 20`, {
481
+ stdio: 'pipe',
482
+ env: { ...process.env, PATH: `${binDir}:${process.env.PATH}` },
483
+ timeout: 300000
484
+ });
485
+
486
+ spinner.succeed('Container runtime started');
487
+ return true;
586
488
  } catch (error) {
587
- log.warning(`Could not install log viewer: ${error.message}`);
489
+ spinner.fail('Failed to start container runtime');
490
+ return false;
588
491
  }
589
492
  }
590
493
 
591
- async function installTribeCLI() {
592
- const spinner = ora('Installing TRIBE CLI...').start();
494
+ async function main() {
495
+ const args = process.argv.slice(2);
593
496
 
594
- 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...';
600
- fs.unlinkSync(tribeDest);
601
- log.success('Removed existing TRIBE CLI');
602
- spinner.text = 'Installing TRIBE CLI...'; // Reset spinner text
603
- }
604
-
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 = '';
497
+ // Handle help
498
+ if (args.includes('--help') || args.includes('-h')) {
499
+ console.log(chalk.bold.blue('TRIBE CLI Installer\n'));
500
+ console.log('Usage: npx @_xtribe/cli [options]\n');
501
+ console.log('Options:');
502
+ console.log(' --help, -h Show this help message');
503
+ console.log(' --skip-cluster Skip cluster deployment');
504
+ console.log(' --dry-run Show what would be installed');
505
+ process.exit(0);
506
+ }
507
+
508
+ // Show welcome
509
+ showWelcome();
510
+
511
+ // Check existing tools
512
+ console.log(chalk.bold('🔍 Checking existing installation...\n'));
513
+
514
+ const existingTools = {
515
+ kubectl: await checkCommand('kubectl'),
516
+ colima: platform === 'darwin' ? await checkCommand('colima') : true,
517
+ tribe: await checkCommand('tribe')
518
+ };
519
+
520
+ const toolsFound = Object.values(existingTools).filter(v => v).length;
521
+ if (toolsFound > 0) {
522
+ console.log(chalk.green(`📦 Found ${toolsFound} existing tool${toolsFound > 1 ? 's' : ''}:`));
523
+ if (existingTools.kubectl) console.log(' ✓ kubectl');
524
+ if (existingTools.colima && platform === 'darwin') console.log(' ✓ colima');
525
+ if (existingTools.tribe) console.log(' tribe');
526
+ console.log('');
527
+ }
528
+
529
+ // Installation
530
+ console.log(chalk.bold('📥 Installing components...\n'));
531
+
532
+ // Install each component
533
+ if (platform === 'darwin') {
534
+ await installColima();
535
+ }
536
+ await installKubectl();
537
+ await installTribeCLI();
538
+
539
+ console.log();
540
+
541
+ // Configuration
542
+ console.log(chalk.bold('🔧 Configuring environment...\n'));
543
+ const configSpinner = ora('Setting up PATH...').start();
544
+ await setupPathEnvironment();
545
+ await setupGlobalNpmCommand();
546
+ configSpinner.succeed('Environment configured');
547
+
548
+ // Start container runtime if needed
549
+ if (platform === 'darwin' && !args.includes('--skip-cluster')) {
550
+ console.log();
551
+ await startColimaWithKubernetes();
552
+ }
553
+
554
+ // Show summary
555
+ showInstallationSummary();
556
+
557
+ process.exit(0);
558
+ }
655
559
 
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
755
-
756
- let downloaded = false;
757
- let lastError;
758
-
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
891
- 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
- try {
1103
- await which('tribe');
1104
- // It's in PATH, no extra message needed
1105
- } 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
- }
1212
- }
1213
- }
1214
-
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');
1239
- return true;
1240
- } catch (error) {
1241
- spinner.fail('Failed to start Colima with Kubernetes');
1242
- log.error(error.message);
1243
- return false;
1244
- }
1245
- }
1246
-
1247
- async function deployTribeCluster() {
1248
- const spinner = ora('Deploying TRIBE cluster...').start();
1249
-
1250
- 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
- }
1344
-
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
- }
1350
-
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
- }
1362
-
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
- }
1397
- }
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
- }
1405
-
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');
1421
- return true;
1422
- } 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
- }
1438
- return false;
1439
- }
1440
- }
1441
-
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
- 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 = [];
1715
-
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 });
1721
- }
1722
-
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
- }
1743
-
1744
- // Verify everything first
1745
- const verified = await verifyInstallation();
1746
-
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
- }
1755
- }
1756
-
1757
- // Try to start container runtime (after showing results)
1758
- const runtimeStarted = await startContainerRuntime();
1759
-
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
- }
1902
-
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
- }
1918
-
1919
- // Exit cleanly
1920
- process.exit(0);
1921
- }
1922
-
1923
- // Handle fetch polyfill for older Node versions
1924
- if (!global.fetch) {
1925
- global.fetch = require('node-fetch');
1926
- }
560
+ // Export for use from index.js
561
+ module.exports = { main };
1927
562
 
563
+ // Run if executed directly
1928
564
  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
- }
565
+ main().catch(error => {
566
+ console.error(chalk.red('Installation failed:'), error.message);
567
+ process.exit(1);
568
+ });
2028
569
  }
2029
-
2030
- module.exports = { main };