@_xtribe/cli 1.0.0-beta.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/install-tribe.js CHANGED
@@ -8,12 +8,17 @@ 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');
21
+ const tribeBinDir = path.join(tribeDir, 'bin');
17
22
 
18
23
  // Ensure local bin directory exists
19
24
  if (!fs.existsSync(binDir)) {
@@ -25,6 +30,11 @@ if (!fs.existsSync(tribeDir)) {
25
30
  fs.mkdirSync(tribeDir, { recursive: true });
26
31
  }
27
32
 
33
+ // Ensure TRIBE bin directory exists
34
+ if (!fs.existsSync(tribeBinDir)) {
35
+ fs.mkdirSync(tribeBinDir, { recursive: true });
36
+ }
37
+
28
38
  const log = {
29
39
  success: (msg) => console.log(chalk.green('✓'), msg),
30
40
  error: (msg) => console.log(chalk.red('✗'), msg),
@@ -42,6 +52,34 @@ async function checkCommand(cmd) {
42
52
  }
43
53
  }
44
54
 
55
+ async function findCommand(cmd) {
56
+ // Try to find command in various locations
57
+ const possiblePaths = [
58
+ path.join(binDir, cmd), // Our install location
59
+ path.join('/opt/homebrew/bin', cmd), // Homebrew on M1 Macs
60
+ path.join('/usr/local/bin', cmd), // Homebrew on Intel Macs
61
+ cmd // In PATH
62
+ ];
63
+
64
+ // First try 'which' command
65
+ try {
66
+ const cmdPath = await which(cmd);
67
+ return cmdPath;
68
+ } catch {
69
+ // If not in PATH, check known locations
70
+ for (const cmdPath of possiblePaths) {
71
+ try {
72
+ await fs.promises.access(cmdPath, fs.constants.X_OK);
73
+ return cmdPath;
74
+ } catch {
75
+ // Continue searching
76
+ }
77
+ }
78
+ }
79
+
80
+ return null;
81
+ }
82
+
45
83
  async function downloadFile(url, dest) {
46
84
  return new Promise((resolve, reject) => {
47
85
  const file = fs.createWriteStream(dest);
@@ -55,59 +93,119 @@ async function downloadFile(url, dest) {
55
93
  return;
56
94
  }
57
95
  response.pipe(file);
58
- file.on('finish', () => {
96
+ file.on('finish', async () => {
59
97
  file.close();
98
+
99
+ // Check if this might be a pointer file
100
+ const stats = fs.statSync(dest);
101
+ if (stats.size < 100) {
102
+ const content = fs.readFileSync(dest, 'utf8').trim();
103
+ // Check if content looks like a path (e.g., "main-121/tribe-linux-amd64")
104
+ if (content.match(/^[\w\-\/]+$/) && content.includes('/')) {
105
+ console.log(`📎 Following pointer to: ${content}`);
106
+ const baseUrl = url.substring(0, url.lastIndexOf('/'));
107
+ const actualBinaryUrl = `${baseUrl}/${content}`;
108
+
109
+ // Download the actual binary
110
+ fs.unlinkSync(dest); // Remove pointer file
111
+ await downloadFile(actualBinaryUrl, dest);
112
+ }
113
+ }
114
+
60
115
  resolve();
61
116
  });
62
117
  }).on('error', reject);
63
118
  });
64
119
  }
65
120
 
66
- async function installDocker() {
67
- if (await checkCommand('docker')) {
68
- // Check if Docker daemon is running
121
+ // Docker installation removed - Colima provides Docker runtime
122
+
123
+ async function installK3s() {
124
+ const spinner = ora('Installing K3s (lightweight Kubernetes)...').start();
125
+
126
+ try {
127
+ // Check if K3s is already installed
69
128
  try {
70
- execSync('docker info', { stdio: 'ignore' });
71
- log.success('Docker is already working');
129
+ execSync('which k3s', { stdio: 'ignore' });
130
+ spinner.succeed('K3s already installed');
72
131
  return true;
73
132
  } catch {
74
- log.warning('Docker CLI found but daemon not running');
133
+ // Not installed, continue
75
134
  }
76
- }
77
-
78
- const spinner = ora('Installing Docker CLI...').start();
79
-
80
- try {
81
- let dockerUrl;
82
- if (platform === 'darwin') {
83
- // Docker uses aarch64 instead of arm64 for macOS
84
- const dockerArch = arch === 'arm64' ? 'aarch64' : 'x86_64';
85
- dockerUrl = `https://download.docker.com/mac/static/stable/${dockerArch}/docker-24.0.7.tgz`;
86
- } else if (platform === 'linux') {
87
- dockerUrl = `https://download.docker.com/linux/static/stable/${arch}/docker-24.0.7.tgz`;
88
- } else {
89
- throw new Error(`Unsupported platform: ${platform}`);
135
+
136
+ // Check if we have permission to install
137
+ const needsSudo = process.getuid && process.getuid() !== 0;
138
+
139
+ // Check if we're in a container or CI environment
140
+ const isContainer = process.env.container === 'docker' ||
141
+ fs.existsSync('/.dockerenv') ||
142
+ !fs.existsSync('/run/systemd/system') ||
143
+ process.env.CI === 'true';
144
+
145
+ if (isContainer) {
146
+ spinner.warn('Running in container/CI - K3s installation skipped');
147
+ log.info('K3s requires systemd and privileged access');
148
+ log.info('For containers, consider using KIND or external cluster');
149
+ return true; // Don't fail in containers
90
150
  }
91
-
92
- const tempFile = path.join(os.tmpdir(), 'docker.tgz');
93
- await downloadFile(dockerUrl, tempFile);
94
151
 
95
- // Extract Docker CLI
96
- execSync(`tar -xzf ${tempFile} -C ${os.tmpdir()}`);
97
- const dockerBinary = path.join(os.tmpdir(), 'docker', 'docker');
98
- const dockerDest = path.join(binDir, 'docker');
152
+ // Download and install K3s
153
+ spinner.text = 'Downloading K3s installer...';
99
154
 
100
- fs.copyFileSync(dockerBinary, dockerDest);
101
- fs.chmodSync(dockerDest, '755');
155
+ const installCommand = needsSudo ?
156
+ 'curl -sfL https://get.k3s.io | sudo sh -s - --write-kubeconfig-mode 644' :
157
+ 'curl -sfL https://get.k3s.io | sh -s - --write-kubeconfig-mode 644';
102
158
 
103
- // Cleanup
104
- fs.rmSync(tempFile);
105
- fs.rmSync(path.join(os.tmpdir(), 'docker'), { recursive: true });
159
+ try {
160
+ execSync(installCommand, {
161
+ stdio: 'pipe',
162
+ env: { ...process.env, INSTALL_K3S_SKIP_START: 'false' }
163
+ });
164
+
165
+ spinner.succeed('K3s installed successfully');
166
+
167
+ // Wait for K3s to be ready
168
+ spinner.text = 'Waiting for K3s to start...';
169
+ await new Promise(resolve => setTimeout(resolve, 10000));
170
+
171
+ // Configure kubectl to use K3s
172
+ const k3sConfig = '/etc/rancher/k3s/k3s.yaml';
173
+ const userConfig = path.join(os.homedir(), '.kube', 'config');
174
+
175
+ if (fs.existsSync(k3sConfig)) {
176
+ fs.mkdirSync(path.dirname(userConfig), { recursive: true });
177
+
178
+ if (needsSudo) {
179
+ execSync(`sudo cp ${k3sConfig} ${userConfig} && sudo chown $(id -u):$(id -g) ${userConfig}`, { stdio: 'ignore' });
180
+ } else {
181
+ fs.copyFileSync(k3sConfig, userConfig);
182
+ }
183
+
184
+ // Update server address to use localhost
185
+ let configContent = fs.readFileSync(userConfig, 'utf8');
186
+ configContent = configContent.replace(/server: https:\/\/127\.0\.0\.1:6443/, 'server: https://localhost:6443');
187
+ fs.writeFileSync(userConfig, configContent);
188
+
189
+ spinner.succeed('K3s configured');
190
+ }
191
+
192
+ return true;
193
+ } catch (error) {
194
+ if (error.message.includes('permission denied') || error.message.includes('sudo')) {
195
+ spinner.fail('K3s installation requires sudo permission');
196
+ log.info('Please run the installer with sudo or install K3s manually:');
197
+ log.info(' curl -sfL https://get.k3s.io | sudo sh -');
198
+ return false;
199
+ }
200
+ throw error;
201
+ }
106
202
 
107
- spinner.succeed('Docker CLI installed');
108
- return true;
109
203
  } catch (error) {
110
- spinner.fail(`Docker CLI installation failed: ${error.message}`);
204
+ spinner.fail('K3s installation failed');
205
+ log.error(error.message);
206
+ log.info('Manual K3s installation:');
207
+ log.info(' curl -sfL https://get.k3s.io | sudo sh -');
208
+ log.info(' sudo systemctl enable --now k3s');
111
209
  return false;
112
210
  }
113
211
  }
@@ -118,8 +216,9 @@ async function installColima() {
118
216
  return true;
119
217
  }
120
218
 
121
- if (await checkCommand('colima')) {
122
- log.success('Colima already installed');
219
+ const existingColima = await findCommand('colima');
220
+ if (existingColima) {
221
+ log.success(`Colima already installed at: ${existingColima}`);
123
222
  return true;
124
223
  }
125
224
 
@@ -162,212 +261,28 @@ async function installColima() {
162
261
  }
163
262
  }
164
263
 
165
- async function installLima() {
166
- if (platform !== 'darwin') {
167
- return true; // Lima only needed on macOS
168
- }
264
+ // Lima installation removed - Colima handles virtualization
169
265
 
170
- if (await checkCommand('limactl')) {
171
- log.success('Lima already installed');
172
- return true;
173
- }
266
+ // KIND installation removed - using Colima's built-in Kubernetes
174
267
 
175
- const spinner = ora('Installing Lima...').start();
176
-
177
- try {
178
- // Strategy 1: Try Homebrew if available (better signing)
179
- try {
180
- execSync('brew --version', { stdio: 'ignore' });
181
- spinner.text = 'Installing Lima via Homebrew...';
182
- execSync('brew install lima', { stdio: 'ignore' });
183
- spinner.succeed('Lima installed via Homebrew');
184
- return true;
185
- } catch (brewError) {
186
- // Homebrew not available, continue with direct download
187
- spinner.text = 'Installing Lima (direct download)...';
188
- }
189
-
190
- // Strategy 2: Direct download
191
- // Get latest Lima release
192
- const response = await fetch('https://api.github.com/repos/lima-vm/lima/releases/latest');
193
- const release = await response.json();
194
- const version = release.tag_name;
195
-
196
- const archName = arch === 'arm64' ? 'arm64' : 'x86_64';
197
- const limaUrl = `https://github.com/lima-vm/lima/releases/download/${version}/lima-${version.replace('v', '')}-Darwin-${archName}.tar.gz`;
198
-
199
- const tempFile = path.join(os.tmpdir(), 'lima.tar.gz');
200
- await downloadFile(limaUrl, tempFile);
201
-
202
- // Extract Lima
203
- const extractDir = path.join(os.tmpdir(), 'lima-extract');
204
- fs.mkdirSync(extractDir, { recursive: true });
205
- execSync(`tar -xzf ${tempFile} -C ${extractDir}`);
206
-
207
- // Lima tarball extracts directly to bin/ directory
208
- const limaBinDir = path.join(extractDir, 'bin');
209
- if (fs.existsSync(limaBinDir)) {
210
- // Copy all Lima binaries
211
- const binaries = fs.readdirSync(limaBinDir);
212
- binaries.forEach(binary => {
213
- const src = path.join(limaBinDir, binary);
214
- const dest = path.join(binDir, binary);
215
- fs.copyFileSync(src, dest);
216
- fs.chmodSync(dest, '755');
217
-
218
- // Remove quarantine attribute
219
- try {
220
- execSync(`xattr -d com.apple.quarantine ${dest}`, { stdio: 'ignore' });
221
- } catch (error) {
222
- // Quarantine attribute may not exist, which is fine
223
- }
224
- });
225
- } else {
226
- throw new Error('Lima binaries not found in expected location');
227
- }
228
-
229
- // Copy share directory (required for guest agents)
230
- const limaShareDir = path.join(extractDir, 'share');
231
- const destShareDir = path.join(homeDir, 'share');
232
- if (fs.existsSync(limaShareDir)) {
233
- if (!fs.existsSync(destShareDir)) {
234
- fs.mkdirSync(destShareDir, { recursive: true });
235
- }
236
-
237
- try {
238
- // Create lima directory in share
239
- const limaDir = path.join(destShareDir, 'lima');
240
- if (!fs.existsSync(limaDir)) {
241
- fs.mkdirSync(limaDir, { recursive: true });
242
- }
243
-
244
- // Strategy 1: Try bundled guest agent (packaged with NPX)
245
- const bundledGuestAgent = path.join(__dirname, 'lima-guestagent.Linux-aarch64.gz');
246
- if (fs.existsSync(bundledGuestAgent)) {
247
- log.info('Using bundled Lima guest agent');
248
- const guestAgentDest = path.join(limaDir, 'lima-guestagent.Linux-aarch64.gz');
249
- fs.copyFileSync(bundledGuestAgent, guestAgentDest);
250
-
251
- // Extract the guest agent (Colima needs it uncompressed)
268
+ async function installKubectl() {
269
+ // Linux with K3s already has kubectl
270
+ if (platform === 'linux') {
252
271
  try {
253
- execSync(`gunzip -f "${guestAgentDest}"`);
254
- } catch (gunzipError) {
255
- // If gunzip fails, try manual extraction
256
- const zlib = require('zlib');
257
- const compressed = fs.readFileSync(guestAgentDest);
258
- const decompressed = zlib.gunzipSync(compressed);
259
- const extractedPath = guestAgentDest.replace('.gz', '');
260
- fs.writeFileSync(extractedPath, decompressed);
261
- fs.unlinkSync(guestAgentDest); // Remove compressed version
262
- }
263
-
264
- // Create basic default template for Colima
265
- const templatesDir = path.join(limaDir, 'templates');
266
- if (!fs.existsSync(templatesDir)) {
267
- fs.mkdirSync(templatesDir, { recursive: true });
268
- }
269
-
270
- const defaultTemplate = `# Basic default template for Colima
271
- images:
272
- - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img"
273
- arch: "x86_64"
274
- - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img"
275
- arch: "aarch64"
276
-
277
- mounts:
278
- - location: "~"
279
- writable: false
280
- - location: "/tmp/lima"
281
- writable: true
282
-
283
- containerd:
284
- system: false
285
- user: false
286
- `;
287
-
288
- fs.writeFileSync(path.join(templatesDir, 'default.yaml'), defaultTemplate);
289
- log.info('Created basic Lima template');
290
-
291
- } else {
292
- // Strategy 2: Try from tarball (fallback)
293
- const guestAgentSrc = path.join(limaShareDir, 'lima-guestagent.Linux-aarch64.gz');
294
- const templatesSrc = path.join(limaShareDir, 'templates');
295
-
296
- if (fs.existsSync(guestAgentSrc)) {
297
- // Copy and extract guest agent
298
- const guestAgentDest = path.join(limaDir, 'lima-guestagent.Linux-aarch64.gz');
299
- fs.copyFileSync(guestAgentSrc, guestAgentDest);
300
-
301
- // Extract the guest agent (Colima needs it uncompressed)
272
+ execSync('k3s kubectl version --client', { stdio: 'ignore' });
273
+ log.success('kubectl available via k3s');
274
+ // Create a symlink for convenience
302
275
  try {
303
- execSync(`gunzip -f "${guestAgentDest}"`);
304
- } catch (gunzipError) {
305
- // If gunzip fails, try manual extraction
306
- const zlib = require('zlib');
307
- const compressed = fs.readFileSync(guestAgentDest);
308
- const decompressed = zlib.gunzipSync(compressed);
309
- const extractedPath = guestAgentDest.replace('.gz', '');
310
- fs.writeFileSync(extractedPath, decompressed);
311
- fs.unlinkSync(guestAgentDest); // Remove compressed version
312
- }
313
- } else {
314
- throw new Error('Lima guest agent not found in tarball or bundled');
315
- }
316
-
317
- if (fs.existsSync(templatesSrc)) {
318
- // Copy templates directory
319
- execSync(`cp -R "${templatesSrc}" "${limaDir}/templates"`);
320
- } else {
321
- // Create minimal template as fallback
322
- const templatesDir = path.join(limaDir, 'templates');
323
- fs.mkdirSync(templatesDir, { recursive: true });
324
- fs.writeFileSync(path.join(templatesDir, 'default.yaml'), defaultTemplate);
325
- }
326
- }
327
-
328
- } catch (error) {
329
- log.warning(`Lima share installation failed: ${error.message}`);
330
- log.warning('You may need to install Lima manually: brew install lima');
276
+ execSync('sudo ln -sf /usr/local/bin/k3s /usr/local/bin/kubectl', { stdio: 'ignore' });
277
+ } catch {
278
+ // Link might already exist
331
279
  }
332
- }
333
-
334
- // Cleanup
335
- fs.rmSync(tempFile);
336
- fs.rmSync(extractDir, { recursive: true });
337
-
338
- spinner.succeed('Lima installed');
339
- return true;
340
- } catch (error) {
341
- spinner.fail(`Lima installation failed: ${error.message}`);
342
- log.warning('You may need to install Lima manually: brew install lima');
343
- return false;
344
- }
345
- }
346
-
347
- async function installKind() {
348
- if (await checkCommand('kind')) {
349
- log.success('KIND already installed');
350
- return true;
351
- }
352
-
353
- const spinner = ora('Installing KIND...').start();
354
-
355
- try {
356
- const kindUrl = `https://kind.sigs.k8s.io/dl/v0.20.0/kind-${platform}-${arch}`;
357
- const kindDest = path.join(binDir, 'kind');
358
-
359
- await downloadFile(kindUrl, kindDest);
360
- fs.chmodSync(kindDest, '755');
361
-
362
- spinner.succeed('KIND installed');
363
280
  return true;
364
- } catch (error) {
365
- spinner.fail(`KIND installation failed: ${error.message}`);
366
- return false;
281
+ } catch {
282
+ // Continue with regular kubectl installation
367
283
  }
368
284
  }
369
285
 
370
- async function installKubectl() {
371
286
  if (await checkCommand('kubectl')) {
372
287
  log.success('kubectl already installed');
373
288
  return true;
@@ -393,166 +308,495 @@ async function installKubectl() {
393
308
  }
394
309
  }
395
310
 
396
- async function installTribeCli() {
311
+ async function setupGlobalNpmCommand() {
312
+ const spinner = ora('Setting up global tribe command...').start();
313
+
314
+ try {
315
+ // Create a temporary directory for the global package
316
+ const tempDir = path.join(os.tmpdir(), 'tribe-global-install-' + Date.now());
317
+ fs.mkdirSync(tempDir, { recursive: true });
318
+
319
+ // Create package.json for global command
320
+ const packageJson = {
321
+ name: 'tribe-cli-global',
322
+ version: '1.0.0',
323
+ description: 'TRIBE CLI global command',
324
+ bin: {
325
+ tribe: './tribe-wrapper.js',
326
+ 'tribe-logs': './tribe-logs-wrapper.js'
327
+ },
328
+ files: ['tribe-wrapper.js', 'tribe-logs-wrapper.js'],
329
+ private: true
330
+ };
331
+
332
+ fs.writeFileSync(
333
+ path.join(tempDir, 'package.json'),
334
+ JSON.stringify(packageJson, null, 2)
335
+ );
336
+
337
+ // Create the log viewer wrapper script
338
+ const logViewerScript = `#!/usr/bin/env node
339
+
340
+ const { spawn } = require('child_process');
341
+ const fs = require('fs');
342
+ const path = require('path');
343
+ const os = require('os');
344
+
345
+ // Path to the actual tribe-logs script
346
+ const logsScriptPath = path.join(os.homedir(), '.tribe', 'bin', 'tribe-logs');
347
+
348
+ // Check if script exists
349
+ if (!fs.existsSync(logsScriptPath)) {
350
+ console.error('Error: tribe-logs script not found.');
351
+ console.error('Please reinstall using: npx @_xtribe/cli --force');
352
+ process.exit(1);
353
+ }
354
+
355
+ // Forward all arguments
356
+ const child = spawn(logsScriptPath, process.argv.slice(2), {
357
+ stdio: 'inherit',
358
+ env: process.env
359
+ });
360
+
361
+ child.on('exit', (code) => {
362
+ process.exit(code || 0);
363
+ });
364
+
365
+ child.on('error', (err) => {
366
+ console.error('Failed to execute tribe-logs:', err.message);
367
+ process.exit(1);
368
+ });`;
369
+
370
+ const logViewerPath = path.join(tempDir, 'tribe-logs-wrapper.js');
371
+ fs.writeFileSync(logViewerPath, logViewerScript);
372
+ fs.chmodSync(logViewerPath, '755');
373
+
374
+ // Create the wrapper script
375
+ const wrapperScript = `#!/usr/bin/env node
376
+
377
+ const { spawn } = require('child_process');
378
+ const fs = require('fs');
379
+ const path = require('path');
380
+ const os = require('os');
381
+
382
+ // Path to the actual tribe binary installed by the installer
383
+ const tribeBinaryPath = path.join(os.homedir(), '.tribe', 'bin', 'tribe');
384
+
385
+ // Check if tribe binary exists
386
+ if (!fs.existsSync(tribeBinaryPath)) {
387
+ console.error('Error: TRIBE CLI binary not found.');
388
+ console.error('Please reinstall using: npx @_xtribe/cli --force');
389
+ process.exit(1);
390
+ }
391
+
392
+ // Make sure binary is executable
393
+ try {
394
+ fs.accessSync(tribeBinaryPath, fs.constants.X_OK);
395
+ } catch (err) {
396
+ console.error('Error: TRIBE CLI binary is not executable.');
397
+ console.error('Please reinstall using: npx @_xtribe/cli --force');
398
+ process.exit(1);
399
+ }
400
+
401
+ // Forward all arguments to the actual tribe binary
402
+ const child = spawn(tribeBinaryPath, process.argv.slice(2), {
403
+ stdio: 'inherit',
404
+ env: process.env
405
+ });
406
+
407
+ child.on('exit', (code) => {
408
+ process.exit(code || 0);
409
+ });
410
+
411
+ child.on('error', (err) => {
412
+ if (err.code === 'ENOENT') {
413
+ console.error('Error: TRIBE CLI binary not found at expected location.');
414
+ console.error('Please reinstall using: npx @_xtribe/cli --force');
415
+ } else {
416
+ console.error('Failed to execute tribe:', err.message);
417
+ }
418
+ process.exit(1);
419
+ });`;
420
+
421
+ const wrapperPath = path.join(tempDir, 'tribe-wrapper.js');
422
+ fs.writeFileSync(wrapperPath, wrapperScript);
423
+ fs.chmodSync(wrapperPath, '755');
424
+
425
+ // Install globally using npm
426
+ spinner.text = 'Installing tribe command globally...';
427
+ try {
428
+ // First try with --force to handle conflicts
429
+ execSync('npm install -g . --force', {
430
+ cwd: tempDir,
431
+ stdio: 'pipe'
432
+ });
433
+ spinner.succeed('Global tribe command installed successfully');
434
+ } catch (error) {
435
+ // Try with sudo if permission denied
436
+ if (error.message.includes('EACCES') || error.message.includes('permission')) {
437
+ spinner.warn('Global installation requires sudo permission');
438
+ log.info('Attempting with sudo...');
439
+ try {
440
+ execSync('sudo npm install -g . --force', {
441
+ cwd: tempDir,
442
+ stdio: 'inherit'
443
+ });
444
+ spinner.succeed('Global tribe command installed successfully (with sudo)');
445
+ } catch (sudoError) {
446
+ throw new Error('Failed to install globally. You may need to run with sudo or fix npm permissions.');
447
+ }
448
+ } else if (error.message.includes('EEXIST')) {
449
+ // Try to remove existing and retry
450
+ spinner.text = 'Removing conflicting global command...';
451
+ try {
452
+ execSync('npm uninstall -g tribe-cli-global @_xtribe/cli', { stdio: 'ignore' });
453
+ execSync('npm install -g . --force', {
454
+ cwd: tempDir,
455
+ stdio: 'pipe'
456
+ });
457
+ spinner.succeed('Global tribe command installed successfully (after cleanup)');
458
+ } catch (retryError) {
459
+ throw new Error('Failed to install globally due to conflicts. Try running: npm uninstall -g @_xtribe/cli');
460
+ }
461
+ } else {
462
+ throw error;
463
+ }
464
+ }
465
+
466
+ // Clean up temp directory
467
+ try {
468
+ fs.rmSync(tempDir, { recursive: true, force: true });
469
+ } catch (cleanupError) {
470
+ // Ignore cleanup errors
471
+ }
472
+
473
+ return true;
474
+ } catch (error) {
475
+ spinner.fail(`Failed to set up global command: ${error.message}`);
476
+ log.info('You can still use tribe via the direct path: ~/.tribe/bin/tribe');
477
+ return false;
478
+ }
479
+ }
480
+
481
+ async function installTribeLogViewer() {
482
+ try {
483
+ // Install the log viewer script
484
+ const logViewerSource = path.join(__dirname, 'tribe-logs.sh');
485
+ const logViewerDest = path.join(tribeBinDir, 'tribe-logs');
486
+
487
+ if (fs.existsSync(logViewerSource)) {
488
+ fs.copyFileSync(logViewerSource, logViewerDest);
489
+ fs.chmodSync(logViewerDest, '755');
490
+ log.success('Installed tribe-logs command for viewing debug logs');
491
+ }
492
+ } catch (error) {
493
+ log.warning(`Could not install log viewer: ${error.message}`);
494
+ }
495
+ }
496
+
497
+ async function installTribeCLI() {
397
498
  const spinner = ora('Installing TRIBE CLI...').start();
398
499
 
399
500
  try {
400
- const tribeDest = path.join(binDir, 'tribe');
401
-
402
- // First check if we have a bundled binary
403
- const bundledBinary = path.join(__dirname, 'tribe');
404
- if (fs.existsSync(bundledBinary)) {
405
- fs.copyFileSync(bundledBinary, tribeDest);
406
- fs.chmodSync(tribeDest, '755');
407
- spinner.succeed('TRIBE CLI installed from bundled binary');
408
- return true;
501
+ const tribeDest = path.join(tribeBinDir, 'tribe');
502
+
503
+ // Unconditionally remove existing binary to ensure a clean install
504
+ if (fs.existsSync(tribeDest)) {
505
+ spinner.text = 'Removing existing TRIBE CLI installation...';
506
+ fs.unlinkSync(tribeDest);
507
+ log.success('Removed existing TRIBE CLI');
508
+ spinner.text = 'Installing TRIBE CLI...'; // Reset spinner text
409
509
  }
410
510
 
411
- // Check if we have local source
412
- const sourceFile = path.join(__dirname, 'cluster-cli.go');
413
- if (fs.existsSync(sourceFile)) {
414
- // Build from source
511
+ // Also remove old location if exists
512
+ const oldTribeDest = path.join(binDir, 'tribe');
513
+ if (fs.existsSync(oldTribeDest)) {
415
514
  try {
416
- execSync('go version', { stdio: 'ignore' });
417
- execSync(`cd ${__dirname} && go build -o tribe cluster-cli.go client.go`);
418
- fs.copyFileSync(path.join(__dirname, 'tribe'), tribeDest);
419
- fs.chmodSync(tribeDest, '755');
420
- spinner.succeed('TRIBE CLI built from source');
421
- return true;
422
- } catch {
423
- spinner.warn('Go not available, trying pre-built binary...');
515
+ fs.unlinkSync(oldTribeDest);
516
+ log.info('Removed old tribe binary from ~/bin');
517
+ } catch (e) {
518
+ // Ignore errors
424
519
  }
425
520
  }
426
521
 
427
- // Try pre-built binary from GitHub
428
- const tribeUrl = `https://github.com/0zen/0zen/releases/latest/download/tribe-${platform}-${arch}`;
522
+ // Download pre-built binary from GitHub
523
+ // Proper architecture detection for all platforms
524
+ let arch;
525
+ if (process.arch === 'x64' || process.arch === 'x86_64') {
526
+ arch = 'amd64';
527
+ } else if (process.arch === 'arm64' || process.arch === 'aarch64') {
528
+ arch = 'arm64';
529
+ } else {
530
+ arch = process.arch; // fallback
531
+ }
532
+
533
+ const platform = os.platform();
429
534
 
535
+ // Multiple sources for reliability
536
+ // For testing, use a direct URL to our releases folder
537
+ const isTestEnv = process.env.TRIBE_TEST_BINARY_URL;
538
+ const githubRepo = process.env.TRIBE_INSTALLER_REPO || 'TRIBE-INC/releases';
539
+
540
+ let downloadUrl = '';
541
+
430
542
  try {
431
- await downloadFile(tribeUrl, tribeDest);
432
- fs.chmodSync(tribeDest, '755');
433
- spinner.succeed('TRIBE CLI installed');
434
- return true;
435
- } catch {
436
- // Fallback: look for any existing tribe binary
437
- const possiblePaths = [
438
- path.join(__dirname, '..', 'tribe-cli'),
439
- path.join(__dirname, '..', 'tribe'),
440
- './tribe-cli',
441
- './tribe'
442
- ];
443
-
444
- for (const possiblePath of possiblePaths) {
445
- if (fs.existsSync(possiblePath)) {
446
- fs.copyFileSync(possiblePath, tribeDest);
447
- fs.chmodSync(tribeDest, '755');
448
- spinner.succeed('TRIBE CLI installed from local binary');
449
- return true;
543
+ spinner.text = 'Fetching latest release information from GitHub...';
544
+ const response = await fetch(`https://api.github.com/repos/${githubRepo}/releases`);
545
+ if (!response.ok) {
546
+ throw new Error(`GitHub API returned ${response.status}`);
547
+ }
548
+ const releases = await response.json();
549
+ if (!releases || releases.length === 0) {
550
+ throw new Error('No releases found');
551
+ }
552
+ const latestRelease = releases[0];
553
+ const assetName = `tribe-${platform}-${arch}`;
554
+ const asset = latestRelease.assets.find(a => a.name === assetName);
555
+
556
+ if (!asset) {
557
+ throw new Error(`No binary found for ${platform}-${arch} in release ${latestRelease.tag_name}`);
558
+ }
559
+
560
+ downloadUrl = asset.browser_download_url;
561
+ spinner.text = `Found latest release: ${latestRelease.tag_name}`;
562
+ } catch (error) {
563
+ spinner.fail(`Failed to get release info: ${error.message}`);
564
+ // Fallback to other methods if the API fails
565
+ }
566
+
567
+ const sources = isTestEnv ? [isTestEnv] : [
568
+ downloadUrl,
569
+ // Public releases repository (primary source)
570
+ `https://raw.githubusercontent.com/${githubRepo}/main/cli/latest-tribe-${platform}-${arch}`,
571
+ // GitHub releases (fallback)
572
+ `https://github.com/${githubRepo}/releases/latest/download/tribe-${platform}-${arch}`,
573
+ // Try jsDelivr CDN (for better availability)
574
+ `https://cdn.jsdelivr.net/gh/${githubRepo}@main/cli/latest-tribe-${platform}-${arch}`,
575
+ ].filter(Boolean); // Filter out empty downloadUrl if API failed
576
+
577
+ let downloaded = false;
578
+ let lastError;
579
+
580
+ for (const url of sources) {
581
+ try {
582
+ let downloadUrl = url;
583
+
584
+ // Check if this is the GitHub API endpoint
585
+ if (url.includes('/api.github.com/')) {
586
+ spinner.text = 'Fetching latest release information from GitHub...';
587
+
588
+ // Fetch release data
589
+ const response = await fetch(url);
590
+ if (!response.ok) {
591
+ throw new Error(`GitHub API returned ${response.status}`);
592
+ }
593
+
594
+ const releaseData = await response.json();
595
+
596
+ // Handle both single release and array of releases
597
+ const release = Array.isArray(releaseData) ? releaseData[0] : releaseData;
598
+
599
+ if (!release || !release.assets) {
600
+ throw new Error('No release found');
601
+ }
602
+
603
+ const assetName = `tribe-${platform}-${arch}`;
604
+ const asset = release.assets.find(a => a.name === assetName);
605
+
606
+ if (!asset) {
607
+ throw new Error(`No binary found for ${platform}-${arch}`);
608
+ }
609
+
610
+ downloadUrl = asset.browser_download_url;
611
+ spinner.text = `Found ${release.prerelease ? 'pre-release' : 'release'}: ${release.tag_name}`;
612
+ }
613
+
614
+ spinner.text = `Downloading TRIBE CLI from ${new URL(downloadUrl).hostname}...`;
615
+ await downloadFile(downloadUrl, tribeDest);
616
+
617
+ // Check if file is not empty
618
+ const stats = fs.statSync(tribeDest);
619
+ if (stats.size === 0) {
620
+ throw new Error('Downloaded file is empty');
450
621
  }
622
+
623
+ fs.chmodSync(tribeDest, '755');
624
+
625
+ // Verify the binary works
626
+ try {
627
+ execSync(`${tribeDest} version`, { stdio: 'ignore' });
628
+ } catch (versionError) {
629
+ // If version fails, still consider it downloaded if file is valid
630
+ log.warning('Binary downloaded but version check failed - may need different architecture');
631
+ }
632
+
633
+ spinner.succeed('TRIBE CLI installed successfully');
634
+
635
+ // Install the log viewer
636
+ await installTribeLogViewer();
637
+
638
+ downloaded = true;
639
+ break;
640
+ } catch (error) {
641
+ lastError = error;
642
+ // Clean up failed download
643
+ if (fs.existsSync(tribeDest)) {
644
+ fs.unlinkSync(tribeDest);
645
+ }
646
+ log.warning(`Failed: ${error.message}. Trying next source...`);
451
647
  }
648
+ }
452
649
 
453
- throw new Error('No TRIBE CLI binary available');
650
+ if (!downloaded) {
651
+ throw new Error(`Failed to download TRIBE CLI. Please check your internet connection and try again.
652
+
653
+ If this persists, the binaries may not be available for your platform (${platform}-${arch}).
654
+ Please visit https://tribecode.ai/support for assistance.`);
454
655
  }
656
+
657
+ return true;
455
658
  } catch (error) {
456
659
  spinner.fail(`TRIBE CLI installation failed: ${error.message}`);
660
+
661
+ // Show helpful support information
662
+ console.log('');
663
+ console.log(chalk.yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
664
+ console.log(chalk.yellow('Need help? Visit:'));
665
+ console.log(chalk.cyan.bold(' https://tribecode.ai/support'));
666
+ console.log('');
667
+ console.log(chalk.gray('You can also:'));
668
+ console.log(chalk.gray(' • Check system requirements'));
669
+ console.log(chalk.gray(' • View troubleshooting guides'));
670
+ console.log(chalk.gray(' • Contact our support team'));
671
+ console.log(chalk.yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
672
+ console.log('');
673
+
457
674
  return false;
458
675
  }
459
676
  }
460
677
 
461
678
  async function startContainerRuntime() {
462
- if (platform !== 'darwin') {
463
- return true; // Linux uses Docker daemon directly
464
- }
679
+ const spinner = ora('Starting container runtime...').start();
465
680
 
466
- // Check if container runtime is already working
467
681
  try {
468
- execSync('docker info', { stdio: 'ignore' });
469
- log.success('Container runtime is already working');
470
- return true;
471
- } catch {
472
- // Try to start Colima with different approaches
473
- if (await checkCommand('colima')) {
474
- const spinner = ora('Starting Colima container runtime...').start();
475
-
682
+ // Platform-specific container runtime handling
683
+ if (platform === 'darwin') {
684
+ // macOS - Check if Colima is running
476
685
  try {
477
- // Strategy 1: Quick start with minimal resources and Kubernetes
478
- spinner.text = 'Starting Colima with Kubernetes...';
479
- execSync('colima start --cpu 2 --memory 4 --disk 10 --kubernetes --vm-type=vz', {
480
- stdio: 'pipe',
481
- timeout: 60000 // 60 second timeout for K8s
482
- });
483
-
484
- // Test if it worked
485
- execSync('docker info', { stdio: 'ignore' });
486
- spinner.succeed('Colima started successfully');
487
- return true;
686
+ const colimaStatus = execSync('colima status 2>&1', { encoding: 'utf8' });
687
+ if (colimaStatus.includes('is running')) {
688
+ spinner.succeed('Colima is already running');
689
+ return true;
690
+ }
691
+ } catch {
692
+ // Colima not running, need to start it
693
+ }
694
+
695
+ // Try to start Colima
696
+ if (await checkCommand('colima')) {
697
+ const spinner = ora('Starting Colima container runtime...').start();
488
698
 
489
- } catch (error) {
490
- // Strategy 2: Start in background with Kubernetes
491
699
  try {
492
- spinner.text = 'Starting Colima with Kubernetes in background...';
493
- const child = spawn(path.join(binDir, 'colima'), ['start', '--cpu', '2', '--memory', '4', '--disk', '10', '--kubernetes'], {
494
- detached: true,
495
- stdio: 'ignore',
496
- env: { ...process.env, PATH: `${binDir}:${process.env.PATH}` }
700
+ // Strategy 1: Quick start with minimal resources and Kubernetes
701
+ spinner.text = 'Starting Colima with Kubernetes...';
702
+ const colimaPath = await findCommand('colima') || path.join(binDir, 'colima');
703
+ execSync(`${colimaPath} start --cpu 2 --memory 4 --disk 10 --kubernetes --vm-type=vz`, {
704
+ stdio: 'pipe',
705
+ timeout: 60000 // 60 second timeout for K8s
497
706
  });
498
- child.unref(); // Don't wait for completion
499
707
 
500
- spinner.succeed('Colima startup initiated (background)');
501
- log.info('Colima is starting in the background');
502
- log.info('Run "colima status" to check progress');
503
- log.info('Run "docker info" to test when ready');
708
+ spinner.succeed('Colima started successfully');
504
709
  return true;
505
710
 
506
- } catch (bgError) {
507
- spinner.fail('Failed to start Colima');
508
- log.warning('Container runtime startup failed (likely due to macOS system restrictions)');
509
- log.info('Options to fix:');
510
- log.info('');
511
- log.info('Option 1 - Manual Colima start:');
512
- log.info(' colima start --cpu 2 --memory 4 # Start container runtime');
513
- log.info(' # This downloads a 344MB disk image (may take time)');
514
- log.info('');
515
- log.info('Option 2 - Use Docker Desktop (easier):');
516
- log.info(' Download from: https://docs.docker.com/desktop/install/mac-install/');
517
- log.info(' # Docker Desktop handles all container runtime setup');
518
- log.info('');
519
- log.info('Option 3 - Use Homebrew (recommended):');
520
- log.info(' brew install colima docker');
521
- log.info(' colima start');
522
- log.info('');
523
- return false;
711
+ } catch (error) {
712
+ // Strategy 2: Start in background with Kubernetes
713
+ try {
714
+ spinner.text = 'Starting Colima with Kubernetes in background...';
715
+ const colimaPath = await findCommand('colima') || path.join(binDir, 'colima');
716
+ const child = spawn(colimaPath, ['start', '--cpu', '2', '--memory', '4', '--disk', '10', '--kubernetes'], {
717
+ detached: true,
718
+ stdio: 'ignore',
719
+ env: { ...process.env, PATH: `${binDir}:${process.env.PATH}` }
720
+ });
721
+ child.unref(); // Don't wait for completion
722
+
723
+ spinner.succeed('Colima startup initiated (background)');
724
+ log.info('Colima is starting in the background');
725
+ log.info('Run "colima status" to check progress');
726
+ return true;
727
+
728
+ } catch (bgError) {
729
+ spinner.fail('Failed to start Colima');
730
+ log.warning('Container runtime startup failed (likely due to macOS system restrictions)');
731
+ log.info('Options to fix:');
732
+ log.info('');
733
+ log.info('Option 1 - Manual Colima start:');
734
+ log.info(' colima start --cpu 2 --memory 4 --kubernetes');
735
+ log.info(' # This downloads a 344MB disk image (may take time)');
736
+ log.info('');
737
+ log.info('Option 2 - Use Homebrew (recommended):');
738
+ log.info(' brew install colima');
739
+ log.info(' colima start --kubernetes');
740
+ log.info('');
741
+ return false;
742
+ }
524
743
  }
525
744
  }
526
- }
527
- }
745
+ } else if (platform === 'linux') {
746
+ // Linux - K3s should already be running
747
+ spinner.text = 'Checking K3s status...';
748
+
749
+ // Check if we're in a container (no systemd)
750
+ const isContainer = process.env.container === 'docker' || fs.existsSync('/.dockerenv') || !fs.existsSync('/run/systemd/system');
751
+
752
+ if (isContainer) {
753
+ spinner.info('Running in container - K3s requires systemd');
754
+ log.info('For containers, use KIND or connect to external cluster');
755
+ return true; // Don't fail
756
+ }
757
+
758
+ try {
759
+ execSync('sudo systemctl is-active --quiet k3s', { stdio: 'ignore' });
760
+ spinner.succeed('K3s is running');
761
+ return true;
762
+ } catch {
763
+ // Try to start K3s
764
+ spinner.text = 'Starting K3s...';
765
+ try {
766
+ execSync('sudo systemctl start k3s', { stdio: 'ignore' });
767
+ await new Promise(resolve => setTimeout(resolve, 5000));
768
+ spinner.succeed('K3s started');
769
+ return true;
770
+ } catch (error) {
771
+ spinner.warn('K3s not available - requires systemd');
772
+ log.info('K3s requires systemd. Install K3s manually or use KIND/external cluster');
773
+ return true; // Don't fail
774
+ }
775
+ }
776
+ } else if (platform === 'win32') {
777
+ spinner.fail('Windows support coming soon!');
778
+ log.info('For Windows, please use WSL2 with Ubuntu and run this installer inside WSL2');
779
+ return false;
780
+ } else {
781
+ spinner.fail(`Unsupported platform: ${platform}`);
528
782
  return false;
529
- }
530
-
531
- async function updatePath() {
532
- const shell = process.env.SHELL || '/bin/zsh';
533
- const rcFile = shell.includes('zsh') ? '.zshrc' : '.bashrc';
534
- const rcPath = path.join(homeDir, rcFile);
535
-
536
- const pathExport = `export PATH="${binDir}:$PATH"`;
537
-
538
- try {
539
- const rcContent = fs.existsSync(rcPath) ? fs.readFileSync(rcPath, 'utf8') : '';
540
- if (!rcContent.includes(pathExport)) {
541
- fs.appendFileSync(rcPath, `\n# Added by TRIBE CLI installer\n${pathExport}\n`);
542
- log.success(`Updated ${rcFile} with PATH`);
543
783
  }
544
784
  } catch (error) {
545
- log.warning(`Failed to update ${rcFile}: ${error.message}`);
785
+ spinner.fail(`Container runtime error: ${error.message}`);
786
+ return false;
546
787
  }
547
-
548
- // Update current process PATH
549
- process.env.PATH = `${binDir}:${process.env.PATH}`;
550
788
  }
551
789
 
790
+ // PATH updates no longer needed - using npm global installation
791
+ // async function updatePath() { ... }
792
+
793
+ // Shell config updates no longer needed - using npm global installation
794
+ // async function updateShellConfig() { ... }
795
+
552
796
  async function verifyInstallation() {
553
797
  const spinner = ora('Verifying installation...').start();
554
798
 
555
- const tools = ['docker', 'kubectl', 'tribe', 'colima'];
799
+ const tools = ['kubectl', 'tribe', 'colima'];
556
800
  const results = {};
557
801
 
558
802
  for (const tool of tools) {
@@ -561,11 +805,27 @@ async function verifyInstallation() {
561
805
 
562
806
  // Special check for container runtime
563
807
  let containerWorking = false;
564
- try {
565
- execSync('docker info', { stdio: 'ignore' });
566
- containerWorking = true;
567
- } catch (error) {
568
- containerWorking = false;
808
+ if (platform === 'darwin') {
809
+ try {
810
+ const colimaStatus = execSync('colima status 2>&1', { encoding: 'utf8' });
811
+ containerWorking = colimaStatus.includes('is running');
812
+ } catch {
813
+ containerWorking = false;
814
+ }
815
+ } else if (platform === 'linux') {
816
+ try {
817
+ // Check if k3s is running
818
+ execSync('systemctl is-active k3s', { stdio: 'ignore' });
819
+ containerWorking = true;
820
+ } catch {
821
+ // Fallback to kubectl
822
+ try {
823
+ execSync('kubectl cluster-info', { stdio: 'ignore' });
824
+ containerWorking = true;
825
+ } catch {
826
+ containerWorking = false;
827
+ }
828
+ }
569
829
  }
570
830
 
571
831
  spinner.stop();
@@ -601,32 +861,83 @@ async function checkClusterExists() {
601
861
 
602
862
  async function checkColimaRunning() {
603
863
  try {
604
- execSync('colima status', { stdio: 'ignore' });
864
+ const colimaPath = await findCommand('colima') || path.join(binDir, 'colima');
865
+ execSync(`${colimaPath} status`, { stdio: 'ignore' });
605
866
  return true;
606
867
  } catch {
607
868
  return false;
608
869
  }
609
870
  }
610
871
 
872
+ async function checkColimaHasKubernetes() {
873
+ try {
874
+ const colimaPath = await findCommand('colima') || path.join(binDir, 'colima');
875
+ const status = execSync(`${colimaPath} status`, { encoding: 'utf8' });
876
+ // Check if kubernetes is mentioned in the status
877
+ return status.toLowerCase().includes('kubernetes');
878
+ } catch {
879
+ return false;
880
+ }
881
+ }
882
+
611
883
  async function startColimaWithKubernetes() {
612
884
  const spinner = ora('Starting Colima with Kubernetes...').start();
613
885
 
614
886
  try {
615
887
  // Check if already running
616
888
  if (await checkColimaRunning()) {
617
- spinner.succeed('Colima is already running');
889
+ // Check if it has Kubernetes enabled
890
+ if (await checkColimaHasKubernetes()) {
891
+ spinner.succeed('Colima is already running with Kubernetes');
892
+
893
+ // Verify kubectl works
894
+ try {
895
+ execSync('kubectl cluster-info', { stdio: 'ignore' });
618
896
  return true;
897
+ } catch {
898
+ spinner.info('Colima is running but kubectl not connected, continuing...');
899
+ }
900
+ } else {
901
+ spinner.text = 'Colima is running without Kubernetes. Restarting with Kubernetes...';
902
+
903
+ // Stop existing Colima
904
+ const colimaPath = await findCommand('colima') || path.join(binDir, 'colima');
905
+ try {
906
+ execSync(`${colimaPath} stop`, {
907
+ stdio: 'pipe',
908
+ timeout: 30000
909
+ });
910
+ // Wait for it to fully stop
911
+ await new Promise(resolve => setTimeout(resolve, 5000));
912
+ } catch (stopError) {
913
+ log.warning('Failed to stop Colima, attempting to start anyway...');
914
+ }
915
+ }
619
916
  }
620
917
 
621
918
  // Start Colima with Kubernetes enabled
622
919
  spinner.text = 'Starting Colima (this may take a few minutes on first run)...';
623
- execSync('colima start --kubernetes --cpu 4 --memory 8 --disk 20', {
920
+ const colimaPath = await findCommand('colima') || path.join(binDir, 'colima');
921
+ execSync(`${colimaPath} start --kubernetes --cpu 4 --memory 8 --disk 20`, {
624
922
  stdio: 'pipe',
625
- env: { ...process.env, PATH: `${binDir}:${process.env.PATH}` }
923
+ env: { ...process.env, PATH: `${binDir}:${process.env.PATH}` },
924
+ timeout: 300000 // 5 minute timeout for first start
626
925
  });
627
926
 
628
- // Verify it's working
629
- execSync('kubectl version --client', { stdio: 'ignore' });
927
+ // Give Colima a moment to stabilize after starting
928
+ spinner.text = 'Waiting for Colima to stabilize...';
929
+ await new Promise(resolve => setTimeout(resolve, 5000));
930
+
931
+ // Verify it's working and set context
932
+ const kubectlPath = await findCommand('kubectl') || 'kubectl';
933
+ execSync(`${kubectlPath} version --client`, { stdio: 'ignore' });
934
+
935
+ // Set kubectl context to colima
936
+ try {
937
+ execSync(`${kubectlPath} config use-context colima`, { stdio: 'ignore' });
938
+ } catch {
939
+ // Context might not exist yet, that's OK
940
+ }
630
941
  spinner.succeed('Colima started with Kubernetes');
631
942
  return true;
632
943
  } catch (error) {
@@ -661,11 +972,37 @@ async function deployTribeCluster() {
661
972
  if (fs.existsSync(sourceYaml)) {
662
973
  fs.copyFileSync(sourceYaml, destYaml);
663
974
  log.info('Copied deployment configuration');
975
+ } else {
976
+ // Download deployment YAML if not bundled
977
+ spinner.text = 'Downloading deployment configuration...';
978
+ const yamlSources = [
979
+ // Direct from GitHub (primary)
980
+ 'https://raw.githubusercontent.com/TRIBE-INC/0zen/main/sdk/cmd/cli-gui/package/tribe-deployment.yaml',
981
+ // jsDelivr CDN as fallback
982
+ 'https://cdn.jsdelivr.net/gh/TRIBE-INC/0zen@main/sdk/cmd/cli-gui/package/tribe-deployment.yaml'
983
+ ];
984
+
985
+ let yamlDownloaded = false;
986
+ for (const yamlUrl of yamlSources) {
987
+ try {
988
+ await downloadFile(yamlUrl, destYaml);
989
+ log.info('Downloaded deployment configuration');
990
+ yamlDownloaded = true;
991
+ break;
992
+ } catch (downloadError) {
993
+ // Try next source
994
+ }
995
+ }
996
+
997
+ if (!yamlDownloaded) {
998
+ spinner.fail('Failed to get deployment configuration');
999
+ throw new Error('Could not download deployment YAML from any source');
1000
+ }
664
1001
  }
665
1002
 
666
1003
  // Run tribe start command with deployment YAML path
667
1004
  spinner.text = 'Running TRIBE cluster deployment...';
668
- const tribePath = path.join(binDir, 'tribe');
1005
+ const tribePath = path.join(tribeBinDir, 'tribe');
669
1006
 
670
1007
  // Set environment variable for the deployment YAML location
671
1008
  const env = {
@@ -674,8 +1011,107 @@ async function deployTribeCluster() {
674
1011
  TRIBE_DEPLOYMENT_YAML: destYaml
675
1012
  };
676
1013
 
677
- // Execute tribe start without validation (services need time to start)
678
- execSync(`${tribePath} start --validate=false`, {
1014
+ // Wait for Kubernetes to be fully ready
1015
+ spinner.text = 'Waiting for Kubernetes to be ready...';
1016
+ const kubectlPath = await findCommand('kubectl') || 'kubectl';
1017
+ let apiReady = false;
1018
+ let contextSet = false;
1019
+
1020
+ // First, wait for the API server to be accessible
1021
+ for (let i = 0; i < 30; i++) {
1022
+ try {
1023
+ // Try to get cluster info - this will work regardless of port
1024
+ const clusterInfo = execSync(`${kubectlPath} cluster-info`, {
1025
+ encoding: 'utf8',
1026
+ env: env,
1027
+ stdio: 'pipe'
1028
+ });
1029
+
1030
+ // Check if we got valid cluster info
1031
+ if (clusterInfo && clusterInfo.includes('is running at')) {
1032
+ apiReady = true;
1033
+ spinner.text = 'Kubernetes API server is ready';
1034
+
1035
+ // Extract the actual API server URL for logging
1036
+ const urlMatch = clusterInfo.match(/is running at (https?:\/\/[^\s]+)/);
1037
+ if (urlMatch) {
1038
+ log.info(`Kubernetes API server: ${urlMatch[1]}`);
1039
+ }
1040
+ break;
1041
+ }
1042
+ } catch (error) {
1043
+ spinner.text = `Waiting for Kubernetes API server... (${i+1}/30)`;
1044
+ await new Promise(resolve => setTimeout(resolve, 2000));
1045
+ }
1046
+ }
1047
+
1048
+ if (!apiReady) {
1049
+ spinner.warn('Kubernetes API server not ready after 60 seconds');
1050
+ log.info('Deployment may fail. You can try running "tribe start" manually later.');
1051
+ return false;
1052
+ }
1053
+
1054
+ // Set the kubectl context to colima
1055
+ spinner.text = 'Setting kubectl context...';
1056
+ try {
1057
+ execSync(`${kubectlPath} config use-context colima`, {
1058
+ stdio: 'ignore',
1059
+ env: env
1060
+ });
1061
+ contextSet = true;
1062
+ } catch {
1063
+ log.warning('Could not set kubectl context to colima');
1064
+ }
1065
+
1066
+ // Wait for the node to be ready
1067
+ spinner.text = 'Waiting for Kubernetes node to be ready...';
1068
+ let nodeReady = false;
1069
+ for (let i = 0; i < 20; i++) {
1070
+ try {
1071
+ // First check if any nodes exist
1072
+ const nodeList = execSync(`${kubectlPath} get nodes -o json`, {
1073
+ encoding: 'utf8',
1074
+ env: env,
1075
+ stdio: 'pipe'
1076
+ });
1077
+
1078
+ const nodes = JSON.parse(nodeList);
1079
+ if (nodes.items && nodes.items.length > 0) {
1080
+ // Now check if the node is ready
1081
+ const nodeStatus = execSync(`${kubectlPath} get nodes -o jsonpath='{.items[0].status.conditions[?(@.type=="Ready")].status}'`, {
1082
+ encoding: 'utf8',
1083
+ env: env,
1084
+ stdio: 'pipe'
1085
+ }).trim();
1086
+
1087
+ if (nodeStatus === 'True') {
1088
+ nodeReady = true;
1089
+ spinner.text = 'Kubernetes node is ready';
1090
+ break;
1091
+ }
1092
+ }
1093
+ } catch (error) {
1094
+ // Ignore errors - node might not be registered yet or connection issues
1095
+ if (error.message && error.message.includes('connection reset')) {
1096
+ spinner.text = `Waiting for Kubernetes node... (${i+1}/20) - reconnecting...`;
1097
+ } else {
1098
+ spinner.text = `Waiting for Kubernetes node... (${i+1}/20)`;
1099
+ }
1100
+ }
1101
+ await new Promise(resolve => setTimeout(resolve, 3000));
1102
+ }
1103
+
1104
+ if (!nodeReady) {
1105
+ spinner.warn('Kubernetes node not ready after 60 seconds');
1106
+ log.info('The cluster may still be initializing. Proceeding with deployment...');
1107
+ }
1108
+
1109
+ // Small delay to let everything stabilize
1110
+ spinner.text = 'Starting TRIBE deployment...';
1111
+ await new Promise(resolve => setTimeout(resolve, 2000));
1112
+
1113
+ // Execute tribe start
1114
+ execSync(`${tribePath} start`, {
679
1115
  stdio: 'pipe',
680
1116
  env: env
681
1117
  });
@@ -689,18 +1125,37 @@ async function deployTribeCluster() {
689
1125
  } catch (error) {
690
1126
  spinner.fail('Failed to deploy TRIBE cluster');
691
1127
  log.error(error.message);
1128
+
1129
+ // Check if this is a Kubernetes connectivity issue
1130
+ if (error.message && error.message.includes('connection refused')) {
1131
+ log.warning('\nIt appears Kubernetes is not ready or not enabled.');
1132
+ log.info('Troubleshooting steps:');
1133
+ log.info('1. Check Colima status: colima status');
1134
+ log.info('2. If running without Kubernetes, restart it:');
1135
+ log.info(' colima stop');
1136
+ log.info(' colima start --kubernetes');
1137
+ log.info('3. Then run: tribe start');
1138
+ } else {
692
1139
  log.info('You can manually deploy later with: tribe start');
1140
+ }
693
1141
  return false;
694
1142
  }
695
1143
  }
696
1144
 
697
1145
  async function promptForClusterSetup() {
1146
+ // Check for auto-approve in CI environments
1147
+ if (process.env.CI === 'true' || process.env.TRIBE_AUTO_APPROVE === 'true') {
1148
+ console.log('\n' + chalk.bold('🚀 TRIBE Cluster Setup'));
1149
+ console.log(chalk.green('Auto-approving cluster setup (CI mode)'));
1150
+ return true;
1151
+ }
1152
+
698
1153
  // Simple prompt without external dependencies
699
1154
  return new Promise((resolve) => {
700
1155
  console.log('\n' + chalk.bold('🚀 TRIBE Cluster Setup'));
701
1156
  console.log('\nWould you like to set up the TRIBE cluster now?');
702
1157
  console.log('This will:');
703
- console.log(' • Start Colima with Kubernetes');
1158
+ console.log(' • Start ' + (platform === 'darwin' ? 'Colima' : 'K3s') + ' with Kubernetes');
704
1159
  console.log(' • Deploy all TRIBE services');
705
1160
  console.log(' • Set up port forwarding');
706
1161
  console.log('\n' + chalk.yellow('Note: This requires ~2GB disk space and may take a few minutes'));
@@ -717,20 +1172,226 @@ async function promptForClusterSetup() {
717
1172
  });
718
1173
  }
719
1174
 
1175
+ async function cleanupOldInstallations() {
1176
+ // Clean up old aliases
1177
+ const rcFiles = ['.zshrc', '.bashrc'];
1178
+ rcFiles.forEach(file => {
1179
+ const rcPath = path.join(homeDir, file);
1180
+ if (fs.existsSync(rcPath)) {
1181
+ try {
1182
+ let content = fs.readFileSync(rcPath, 'utf8');
1183
+ const originalContent = content;
1184
+
1185
+ // Remove old tribe-cli aliases
1186
+ content = content.replace(/^alias tribe=['"]tribe-cli['"]\s*$/gm, '# $& # Removed by TRIBE installer');
1187
+ content = content.replace(/^alias t=['"]tribe-cli['"]\s*$/gm, '# $& # Removed by TRIBE installer');
1188
+
1189
+ if (content !== originalContent) {
1190
+ fs.writeFileSync(rcPath, content);
1191
+ log.info(`Cleaned up old aliases in ${file}`);
1192
+ }
1193
+ } catch (error) {
1194
+ log.warning(`Could not clean up ${file}: ${error.message}`);
1195
+ }
1196
+ }
1197
+ });
1198
+ }
1199
+
1200
+ async function forceCleanInstallation() {
1201
+ console.log(chalk.yellow('\n🧹 Performing force clean installation...\n'));
1202
+
1203
+ // Uninstall global npm package if exists
1204
+ try {
1205
+ execSync('npm uninstall -g tribe-cli-global', {
1206
+ stdio: 'ignore'
1207
+ });
1208
+ log.success('Removed global tribe command');
1209
+ } catch (error) {
1210
+ // It might not be installed, which is fine
1211
+ }
1212
+
1213
+ // List of binaries to remove from ~/bin
1214
+ const binariesToRemove = ['tribe', 'kubectl', 'colima'];
1215
+
1216
+ for (const binary of binariesToRemove) {
1217
+ const binaryPath = path.join(binDir, binary);
1218
+ if (fs.existsSync(binaryPath)) {
1219
+ try {
1220
+ fs.unlinkSync(binaryPath);
1221
+ log.success(`Removed existing ${binary} binary from ~/bin`);
1222
+ } catch (error) {
1223
+ log.warning(`Could not remove ${binary}: ${error.message}`);
1224
+ }
1225
+ }
1226
+ }
1227
+
1228
+ // Also clean from .tribe/bin
1229
+ const tribeBinariesToRemove = ['tribe'];
1230
+ for (const binary of tribeBinariesToRemove) {
1231
+ const binaryPath = path.join(tribeBinDir, binary);
1232
+ if (fs.existsSync(binaryPath)) {
1233
+ try {
1234
+ fs.unlinkSync(binaryPath);
1235
+ log.success(`Removed existing ${binary} binary from ~/.tribe/bin`);
1236
+ } catch (error) {
1237
+ log.warning(`Could not remove ${binary}: ${error.message}`);
1238
+ }
1239
+ }
1240
+ }
1241
+
1242
+ // Clean up TRIBE configuration directory
1243
+ if (fs.existsSync(tribeDir)) {
1244
+ try {
1245
+ // Keep important configs but remove deployment markers
1246
+ const deploymentMarker = path.join(tribeDir, '.cluster-deployed');
1247
+ if (fs.existsSync(deploymentMarker)) {
1248
+ fs.unlinkSync(deploymentMarker);
1249
+ log.success('Removed cluster deployment marker');
1250
+ }
1251
+
1252
+ // Remove cached deployment YAML
1253
+ const deploymentYaml = path.join(tribeDir, 'tribe-deployment.yaml');
1254
+ if (fs.existsSync(deploymentYaml)) {
1255
+ fs.unlinkSync(deploymentYaml);
1256
+ log.success('Removed cached deployment configuration');
1257
+ }
1258
+ } catch (error) {
1259
+ log.warning(`Could not clean TRIBE directory: ${error.message}`);
1260
+ }
1261
+ }
1262
+
1263
+ // Clean up npm cache for @_xtribe/cli
1264
+ try {
1265
+ execSync('npm cache clean --force @_xtribe/cli', {
1266
+ stdio: 'ignore',
1267
+ timeout: 10000
1268
+ });
1269
+ log.success('Cleared npm cache for @_xtribe/cli');
1270
+ } catch (error) {
1271
+ // npm cache clean might fail, but that's okay
1272
+ log.info('npm cache clean attempted (may have failed, which is okay)');
1273
+ }
1274
+
1275
+ // Platform-specific cleanup
1276
+ if (platform === 'darwin') {
1277
+ // Check if Colima is running and stop it
1278
+ try {
1279
+ const colimaPath = await findCommand('colima');
1280
+ if (colimaPath) {
1281
+ const status = execSync(`${colimaPath} status 2>&1`, { encoding: 'utf8' });
1282
+ if (status.includes('is running')) {
1283
+ log.info('Stopping Colima...');
1284
+ execSync(`${colimaPath} stop`, { stdio: 'ignore', timeout: 30000 });
1285
+ log.success('Stopped Colima');
1286
+ }
1287
+ }
1288
+ } catch (error) {
1289
+ // Colima might not be installed or running
1290
+ }
1291
+ } else if (platform === 'linux') {
1292
+ // Note about K3s - requires manual uninstall
1293
+ try {
1294
+ execSync('which k3s', { stdio: 'ignore' });
1295
+ log.warning('K3s detected. To fully remove K3s, run: /usr/local/bin/k3s-uninstall.sh');
1296
+ log.info('The installer will proceed, but K3s will remain installed');
1297
+ } catch {
1298
+ // K3s not installed
1299
+ }
1300
+ }
1301
+
1302
+ console.log(chalk.green('\n✓ Clean installation preparation complete\n'));
1303
+ }
1304
+
720
1305
  async function main() {
721
1306
  console.log(chalk.bold.blue('\n🚀 TRIBE CLI Complete Installer\n'));
722
1307
 
1308
+ // Detect and display platform info
1309
+ const displayArch = process.arch === 'x64' ? 'amd64' : process.arch;
1310
+ console.log(chalk.cyan(`Platform: ${platform} (${displayArch})`));
1311
+ console.log(chalk.cyan(`Node: ${process.version}`));
1312
+ console.log('');
1313
+
1314
+ // Check for unsupported platforms early
1315
+ if (platform === 'win32') {
1316
+ console.log(chalk.yellow('\n⚠️ Windows is not yet supported'));
1317
+ console.log(chalk.yellow('Please use WSL2 with Ubuntu and run this installer inside WSL2'));
1318
+ console.log(chalk.yellow('Instructions: https://docs.microsoft.com/en-us/windows/wsl/install\n'));
1319
+ process.exit(1);
1320
+ }
1321
+
1322
+ // Edge case detection
1323
+ // 1. Check for unsupported architectures
1324
+ const supportedArchitectures = ['x64', 'x86_64', 'arm64', 'aarch64'];
1325
+ if (!supportedArchitectures.includes(nodeArch)) {
1326
+ console.log(chalk.red('\n❌ Unsupported Architecture'));
1327
+ console.log(chalk.red(`Your system architecture (${nodeArch}) is not supported.`));
1328
+ console.log(chalk.yellow('TRIBE only provides binaries for x64 (AMD64) and ARM64.\n'));
1329
+ process.exit(1);
1330
+ }
1331
+
1332
+ // 2. WSL2 detection
1333
+ if (fs.existsSync('/proc/sys/fs/binfmt_misc/WSLInterop') || process.env.WSL_DISTRO_NAME) {
1334
+ console.log(chalk.yellow('\n⚠️ WSL2 Environment Detected'));
1335
+ console.log(chalk.yellow('K3s may have networking limitations in WSL2.'));
1336
+ console.log(chalk.yellow('Consider using Docker Desktop with WSL2 backend instead.\n'));
1337
+ // Don't exit, just warn
1338
+ }
1339
+
1340
+ // 3. Proxy detection
1341
+ const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
1342
+ if (proxyUrl) {
1343
+ console.log(chalk.blue('ℹ️ Proxy detected: ' + proxyUrl));
1344
+ console.log(chalk.blue('Downloads will use system proxy settings.\n'));
1345
+ }
1346
+
1347
+ // 4. Check for existing Kubernetes installations
1348
+ const k8sTools = [];
1349
+ const checkTools = ['minikube', 'microk8s', 'kind', 'k3d', 'kubectl'];
1350
+ for (const tool of checkTools) {
1351
+ try {
1352
+ execSync(`which ${tool}`, { stdio: 'ignore' });
1353
+ k8sTools.push(tool);
1354
+ } catch {}
1355
+ }
1356
+
1357
+ if (k8sTools.length > 0) {
1358
+ console.log(chalk.blue('\n📦 Existing Kubernetes tools detected: ' + k8sTools.join(', ')));
1359
+ console.log(chalk.blue('You can use --skip-cluster to skip K3s installation.\n'));
1360
+ }
1361
+
1362
+ // 5. Disk space check
1363
+ try {
1364
+ const stats = fs.statfsSync(homeDir);
1365
+ const freeGB = (stats.bavail * stats.bsize) / (1024 ** 3);
1366
+ if (freeGB < 3) {
1367
+ console.log(chalk.yellow(`\n⚠️ Low disk space: ${freeGB.toFixed(1)}GB free`));
1368
+ console.log(chalk.yellow('K3s installation requires at least 3GB of free space.\n'));
1369
+ }
1370
+ } catch {}
1371
+
1372
+ // Clean up old installations
1373
+ await cleanupOldInstallations();
1374
+
723
1375
  log.info(`Detected: ${platform} (${arch})`);
724
- log.info(`Installing to: ${binDir}`);
1376
+ log.info(`Installing to: ${tribeBinDir}`);
725
1377
 
726
- // Update PATH first
727
- await updatePath();
1378
+ // No longer updating PATH - will use npm global install instead
1379
+
1380
+ const tasks = [];
1381
+
1382
+ // Platform-specific Kubernetes installation
1383
+ if (platform === 'darwin') {
1384
+ tasks.push({ name: 'Colima', fn: installColima });
1385
+ } else if (platform === 'linux') {
1386
+ tasks.push({ name: 'K3s', fn: installK3s });
1387
+ }
728
1388
 
729
- const tasks = [
730
- { name: 'Colima', fn: installColima },
1389
+ // Common tools for all platforms
1390
+ tasks.push(
731
1391
  { name: 'kubectl', fn: installKubectl },
732
- { name: 'TRIBE CLI', fn: installTribeCli }
733
- ];
1392
+ { name: 'TRIBE CLI', fn: installTribeCLI },
1393
+ { name: 'Global tribe command', fn: setupGlobalNpmCommand }
1394
+ );
734
1395
 
735
1396
  let allSuccess = true;
736
1397
 
@@ -750,7 +1411,7 @@ async function main() {
750
1411
  // Check if cluster already exists
751
1412
  const clusterExists = await checkClusterExists();
752
1413
 
753
- if (!clusterExists) {
1414
+ if (!clusterExists && !global.skipCluster) {
754
1415
  // Prompt for cluster setup
755
1416
  const shouldSetup = await promptForClusterSetup();
756
1417
 
@@ -774,6 +1435,14 @@ async function main() {
774
1435
  if (deployed) {
775
1436
  console.log('\n' + chalk.bold.green('✨ TRIBE is ready!'));
776
1437
  console.log('');
1438
+
1439
+ // Provide immediate access to tribe command
1440
+ log.info('TRIBE command is now available globally!');
1441
+ console.log(chalk.green(' tribe # Run from anywhere'));
1442
+ console.log(chalk.gray(' tribe --help # View available commands'));
1443
+ console.log(chalk.gray(' tribe status # Check cluster status'));
1444
+ console.log('');
1445
+
777
1446
  log.info('Quick start:');
778
1447
  console.log(' tribe # Launch interactive CLI');
779
1448
  console.log(' tribe status # Check cluster status');
@@ -793,6 +1462,12 @@ async function main() {
793
1462
  console.log('\n' + chalk.bold.green('✨ TRIBE is ready!'));
794
1463
  log.success('Cluster is already running');
795
1464
  console.log('');
1465
+
1466
+ // Provide immediate access to tribe command
1467
+ log.info('TRIBE command is now available globally!');
1468
+ console.log(chalk.green(' tribe # Run from anywhere'));
1469
+ console.log('');
1470
+
796
1471
  log.info('Commands:');
797
1472
  console.log(' tribe # Launch interactive CLI');
798
1473
  console.log(' tribe status # Check status');
@@ -804,17 +1479,30 @@ async function main() {
804
1479
  console.log('');
805
1480
  // Check container runtime for guidance
806
1481
  let runtimeWorking = false;
807
- try {
808
- execSync('docker info', { stdio: 'ignore' });
809
- runtimeWorking = true;
810
- } catch {
811
- runtimeWorking = false;
812
- }
813
-
814
- if (!runtimeWorking) {
815
- log.info('Start container runtime:');
816
- console.log(' colima start # macOS');
817
- console.log(' sudo systemctl start docker # Linux');
1482
+ if (platform === 'darwin') {
1483
+ try {
1484
+ const colimaStatus = execSync('colima status 2>&1', { encoding: 'utf8' });
1485
+ runtimeWorking = colimaStatus.includes('is running');
1486
+ } catch {
1487
+ runtimeWorking = false;
1488
+ }
1489
+
1490
+ if (!runtimeWorking) {
1491
+ log.info('Start container runtime:');
1492
+ console.log(' colima start --kubernetes # Start Colima with Kubernetes');
1493
+ }
1494
+ } else if (platform === 'linux') {
1495
+ try {
1496
+ execSync('systemctl is-active k3s', { stdio: 'ignore' });
1497
+ runtimeWorking = true;
1498
+ } catch {
1499
+ runtimeWorking = false;
1500
+ }
1501
+
1502
+ if (!runtimeWorking) {
1503
+ log.info('Start container runtime:');
1504
+ console.log(' sudo systemctl start k3s # Start k3s');
1505
+ }
818
1506
  }
819
1507
  console.log('');
820
1508
  log.info('Restart your shell or run: source ~/.zshrc');
@@ -832,13 +1520,25 @@ if (require.main === module) {
832
1520
 
833
1521
  if (args.includes('--help') || args.includes('-h')) {
834
1522
  console.log(chalk.bold.blue('TRIBE CLI Installer\n'));
835
- console.log('Usage: npx tribe-cli-local [options]\n');
1523
+ console.log('Usage: npx @_xtribe/cli [options]\n');
836
1524
  console.log('Options:');
837
1525
  console.log(' --help, -h Show this help message');
838
1526
  console.log(' --verify Only verify existing installation');
839
1527
  console.log(' --dry-run Show what would be installed');
840
- console.log('\nThis installer sets up the complete TRIBE development environment:');
841
- console.log(' Colima (Container runtime with Kubernetes)');
1528
+ console.log(' --skip-cluster Skip cluster deployment (for testing)');
1529
+ console.log(' --force Force clean installation (removes existing installations)');
1530
+ console.log('\nThis installer sets up the complete TRIBE development environment:');
1531
+
1532
+ if (platform === 'darwin') {
1533
+ console.log('• Colima (Container runtime with Kubernetes)');
1534
+ } else if (platform === 'linux') {
1535
+ console.log('• K3s (Lightweight Kubernetes, one-line install)');
1536
+ } else if (platform === 'win32') {
1537
+ console.log('\n⚠️ Windows support coming soon!');
1538
+ console.log('Please use WSL2 with Ubuntu and run this installer inside WSL2');
1539
+ process.exit(1);
1540
+ }
1541
+
842
1542
  console.log('• kubectl (Kubernetes CLI)');
843
1543
  console.log('• TRIBE CLI (Multi-agent orchestration)');
844
1544
  console.log('\nAfter installation:');
@@ -860,19 +1560,32 @@ if (require.main === module) {
860
1560
  log.info(`Platform: ${platform} (${arch})`);
861
1561
  log.info(`Install directory: ${binDir}`);
862
1562
  console.log('\nWould install:');
863
- console.log('• Docker CLI (if not present)');
864
- console.log('• Colima container runtime (macOS only)');
865
- console.log('• Lima virtualization (macOS only)');
866
- console.log('• Colima - Container runtime with Kubernetes');
1563
+ console.log('• Colima - Container runtime with Kubernetes (macOS only)');
867
1564
  console.log('• kubectl - Kubernetes CLI');
868
1565
  console.log('• TRIBE CLI - Multi-agent system');
869
1566
  process.exit(0);
870
1567
  }
871
1568
 
872
- main().catch(error => {
873
- console.error(chalk.red('Installation failed:'), error.message);
874
- process.exit(1);
875
- });
1569
+ // Store skip-cluster flag
1570
+ global.skipCluster = args.includes('--skip-cluster');
1571
+
1572
+ // Check for force flag
1573
+ if (args.includes('--force')) {
1574
+ forceCleanInstallation().then(() => {
1575
+ main().catch(error => {
1576
+ console.error(chalk.red('Installation failed:'), error.message);
1577
+ process.exit(1);
1578
+ });
1579
+ }).catch(error => {
1580
+ console.error(chalk.red('Force clean failed:'), error.message);
1581
+ process.exit(1);
1582
+ });
1583
+ } else {
1584
+ main().catch(error => {
1585
+ console.error(chalk.red('Installation failed:'), error.message);
1586
+ process.exit(1);
1587
+ });
1588
+ }
876
1589
  }
877
1590
 
878
1591
  module.exports = { main };