@_xtribe/cli 1.0.0-beta.4 → 1.0.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,881 @@
1
+ #!/usr/bin/env node
2
+
3
+ const os = require('os');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const https = require('https');
7
+ const { execSync, spawn } = require('child_process');
8
+ const chalk = require('chalk');
9
+ const ora = require('ora');
10
+ const which = require('which');
11
+
12
+ const platform = os.platform();
13
+ const arch = os.arch();
14
+ const homeDir = os.homedir();
15
+ const binDir = path.join(homeDir, 'bin');
16
+ const tribeDir = path.join(homeDir, '.tribe');
17
+
18
+ // Ensure local bin directory exists
19
+ if (!fs.existsSync(binDir)) {
20
+ fs.mkdirSync(binDir, { recursive: true });
21
+ }
22
+
23
+ // Ensure TRIBE config directory exists
24
+ if (!fs.existsSync(tribeDir)) {
25
+ fs.mkdirSync(tribeDir, { recursive: true });
26
+ }
27
+
28
+ const log = {
29
+ success: (msg) => console.log(chalk.green('✓'), msg),
30
+ error: (msg) => console.log(chalk.red('✗'), msg),
31
+ warning: (msg) => console.log(chalk.yellow('⚠'), msg),
32
+ info: (msg) => console.log(chalk.blue('ℹ'), msg),
33
+ step: (msg) => console.log(chalk.cyan('→'), msg)
34
+ };
35
+
36
+ async function checkCommand(cmd) {
37
+ try {
38
+ await which(cmd);
39
+ return true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ async function downloadFile(url, dest) {
46
+ return new Promise((resolve, reject) => {
47
+ const file = fs.createWriteStream(dest);
48
+ https.get(url, (response) => {
49
+ if (response.statusCode === 302 || response.statusCode === 301) {
50
+ // Handle redirects
51
+ return downloadFile(response.headers.location, dest).then(resolve, reject);
52
+ }
53
+ if (response.statusCode !== 200) {
54
+ reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
55
+ return;
56
+ }
57
+ response.pipe(file);
58
+ file.on('finish', () => {
59
+ file.close();
60
+ resolve();
61
+ });
62
+ }).on('error', reject);
63
+ });
64
+ }
65
+
66
+ async function installDocker() {
67
+ if (await checkCommand('docker')) {
68
+ // Check if Docker daemon is running
69
+ try {
70
+ execSync('docker info', { stdio: 'ignore' });
71
+ log.success('Docker is already working');
72
+ return true;
73
+ } catch {
74
+ log.warning('Docker CLI found but daemon not running');
75
+ }
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}`);
90
+ }
91
+
92
+ const tempFile = path.join(os.tmpdir(), 'docker.tgz');
93
+ await downloadFile(dockerUrl, tempFile);
94
+
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');
99
+
100
+ fs.copyFileSync(dockerBinary, dockerDest);
101
+ fs.chmodSync(dockerDest, '755');
102
+
103
+ // Cleanup
104
+ fs.rmSync(tempFile);
105
+ fs.rmSync(path.join(os.tmpdir(), 'docker'), { recursive: true });
106
+
107
+ spinner.succeed('Docker CLI installed');
108
+ return true;
109
+ } catch (error) {
110
+ spinner.fail(`Docker CLI installation failed: ${error.message}`);
111
+ return false;
112
+ }
113
+ }
114
+
115
+ async function installColima() {
116
+ if (platform !== 'darwin') {
117
+ log.info('Colima is only needed on macOS - skipping');
118
+ return true;
119
+ }
120
+
121
+ if (await checkCommand('colima')) {
122
+ log.success('Colima already installed');
123
+ return true;
124
+ }
125
+
126
+ const spinner = ora('Installing Colima...').start();
127
+
128
+ try {
129
+ // Strategy 1: Try Homebrew if available (better signing)
130
+ try {
131
+ execSync('brew --version', { stdio: 'ignore' });
132
+ spinner.text = 'Installing Colima via Homebrew...';
133
+ execSync('brew install colima', { stdio: 'ignore' });
134
+ spinner.succeed('Colima installed via Homebrew');
135
+ return true;
136
+ } catch (brewError) {
137
+ // Homebrew not available, continue with direct download
138
+ spinner.text = 'Installing Colima (direct download)...';
139
+ }
140
+
141
+ // Strategy 2: Direct download with Gatekeeper approval
142
+ const colimaUrl = `https://github.com/abiosoft/colima/releases/latest/download/colima-${platform}-${arch}`;
143
+ const colimaDest = path.join(binDir, 'colima');
144
+
145
+ await downloadFile(colimaUrl, colimaDest);
146
+ fs.chmodSync(colimaDest, '755');
147
+
148
+ // Try to remove quarantine attribute (macOS Sequoia workaround)
149
+ try {
150
+ execSync(`xattr -d com.apple.quarantine ${colimaDest}`, { stdio: 'ignore' });
151
+ log.info('Removed quarantine attribute from Colima');
152
+ } catch (error) {
153
+ // Quarantine attribute may not exist, which is fine
154
+ log.info('Colima installed (quarantine handling not needed)');
155
+ }
156
+
157
+ spinner.succeed('Colima installed');
158
+ return true;
159
+ } catch (error) {
160
+ spinner.fail(`Colima installation failed: ${error.message}`);
161
+ return false;
162
+ }
163
+ }
164
+
165
+ async function installLima() {
166
+ if (platform !== 'darwin') {
167
+ return true; // Lima only needed on macOS
168
+ }
169
+
170
+ if (await checkCommand('limactl')) {
171
+ log.success('Lima already installed');
172
+ return true;
173
+ }
174
+
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)
252
+ 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)
302
+ 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');
331
+ }
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
+ return true;
364
+ } catch (error) {
365
+ spinner.fail(`KIND installation failed: ${error.message}`);
366
+ return false;
367
+ }
368
+ }
369
+
370
+ async function installKubectl() {
371
+ if (await checkCommand('kubectl')) {
372
+ log.success('kubectl already installed');
373
+ return true;
374
+ }
375
+
376
+ const spinner = ora('Installing kubectl...').start();
377
+
378
+ try {
379
+ // Get latest stable version
380
+ const versionResponse = await fetch('https://dl.k8s.io/release/stable.txt');
381
+ const version = await versionResponse.text();
382
+ const kubectlUrl = `https://dl.k8s.io/release/${version.trim()}/bin/${platform}/${arch}/kubectl`;
383
+ const kubectlDest = path.join(binDir, 'kubectl');
384
+
385
+ await downloadFile(kubectlUrl, kubectlDest);
386
+ fs.chmodSync(kubectlDest, '755');
387
+
388
+ spinner.succeed('kubectl installed');
389
+ return true;
390
+ } catch (error) {
391
+ spinner.fail(`kubectl installation failed: ${error.message}`);
392
+ return false;
393
+ }
394
+ }
395
+
396
+ async function installTribeCli() {
397
+ const spinner = ora('Installing TRIBE CLI...').start();
398
+
399
+ 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;
409
+ }
410
+
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
415
+ 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...');
424
+ }
425
+ }
426
+
427
+ // Try pre-built binary from GitHub
428
+ const tribeUrl = `https://github.com/0zen/0zen/releases/latest/download/tribe-${platform}-${arch}`;
429
+
430
+ 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;
450
+ }
451
+ }
452
+
453
+ throw new Error('No TRIBE CLI binary available');
454
+ }
455
+ } catch (error) {
456
+ spinner.fail(`TRIBE CLI installation failed: ${error.message}`);
457
+ return false;
458
+ }
459
+ }
460
+
461
+ async function startContainerRuntime() {
462
+ if (platform !== 'darwin') {
463
+ return true; // Linux uses Docker daemon directly
464
+ }
465
+
466
+ // Check if container runtime is already working
467
+ 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
+
476
+ try {
477
+ // Strategy 1: Quick start with minimal resources
478
+ spinner.text = 'Starting Colima (minimal setup)...';
479
+ execSync('colima start --cpu 1 --memory 2 --disk 5 --vm-type=vz', {
480
+ stdio: 'pipe',
481
+ timeout: 30000 // 30 second timeout
482
+ });
483
+
484
+ // Test if it worked
485
+ execSync('docker info', { stdio: 'ignore' });
486
+ spinner.succeed('Colima started successfully');
487
+ return true;
488
+
489
+ } catch (error) {
490
+ // Strategy 2: Start in background and don't wait
491
+ try {
492
+ spinner.text = 'Starting Colima in background...';
493
+ const child = spawn(path.join(binDir, 'colima'), ['start', '--cpu', '2', '--memory', '4'], {
494
+ detached: true,
495
+ stdio: 'ignore',
496
+ env: { ...process.env, PATH: `${binDir}:${process.env.PATH}` }
497
+ });
498
+ child.unref(); // Don't wait for completion
499
+
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');
504
+ return true;
505
+
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;
524
+ }
525
+ }
526
+ }
527
+ }
528
+ 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
+ }
544
+ } catch (error) {
545
+ log.warning(`Failed to update ${rcFile}: ${error.message}`);
546
+ }
547
+
548
+ // Update current process PATH
549
+ process.env.PATH = `${binDir}:${process.env.PATH}`;
550
+ }
551
+
552
+ async function verifyInstallation() {
553
+ const spinner = ora('Verifying installation...').start();
554
+
555
+ const tools = ['docker', 'kind', 'kubectl', 'tribe'];
556
+ const results = {};
557
+
558
+ for (const tool of tools) {
559
+ results[tool] = await checkCommand(tool);
560
+ }
561
+
562
+ // Special check for container runtime
563
+ let containerWorking = false;
564
+ try {
565
+ execSync('docker info', { stdio: 'ignore' });
566
+ containerWorking = true;
567
+ } catch (error) {
568
+ containerWorking = false;
569
+ }
570
+
571
+ spinner.stop();
572
+
573
+ console.log('\n' + chalk.bold('Installation Summary:'));
574
+ tools.forEach(tool => {
575
+ const status = results[tool] ? chalk.green('✓') : chalk.red('✗');
576
+ console.log(`${status} ${tool}`);
577
+ });
578
+
579
+ const extraStatus = containerWorking ? chalk.green('✓') : chalk.yellow('⚠');
580
+ console.log(`${extraStatus} Container runtime`);
581
+
582
+ if (platform === 'darwin') {
583
+ const colimaInstalled = await checkCommand('colima');
584
+ const limaInstalled = await checkCommand('limactl');
585
+ console.log(`${colimaInstalled ? chalk.green('✓') : chalk.yellow('⚠')} Colima`);
586
+ console.log(`${limaInstalled ? chalk.green('✓') : chalk.yellow('⚠')} Lima`);
587
+ }
588
+
589
+ return Object.values(results).every(r => r) && containerWorking;
590
+ }
591
+
592
+ async function checkClusterExists() {
593
+ try {
594
+ // Check if TRIBE namespace exists in any context
595
+ execSync('kubectl get namespace tribe-system', { stdio: 'ignore' });
596
+ return true;
597
+ } catch {
598
+ return false;
599
+ }
600
+ }
601
+
602
+ async function checkColimaRunning() {
603
+ try {
604
+ execSync('colima status', { stdio: 'ignore' });
605
+ return true;
606
+ } catch {
607
+ return false;
608
+ }
609
+ }
610
+
611
+ async function startColimaWithKubernetes() {
612
+ const spinner = ora('Starting Colima with Kubernetes...').start();
613
+
614
+ try {
615
+ // Check if already running
616
+ if (await checkColimaRunning()) {
617
+ spinner.succeed('Colima is already running');
618
+ return true;
619
+ }
620
+
621
+ // Start Colima with Kubernetes enabled
622
+ spinner.text = 'Starting Colima (this may take a few minutes on first run)...';
623
+ execSync('colima start --kubernetes --cpu 4 --memory 8 --disk 20', {
624
+ stdio: 'pipe',
625
+ env: { ...process.env, PATH: `${binDir}:${process.env.PATH}` }
626
+ });
627
+
628
+ // Verify it's working
629
+ execSync('kubectl version --client', { stdio: 'ignore' });
630
+ spinner.succeed('Colima started with Kubernetes');
631
+ return true;
632
+ } catch (error) {
633
+ spinner.fail('Failed to start Colima with Kubernetes');
634
+ log.error(error.message);
635
+ return false;
636
+ }
637
+ }
638
+
639
+ async function deployTribeCluster() {
640
+ const spinner = ora('Deploying TRIBE cluster...').start();
641
+
642
+ try {
643
+ // Create a marker file to indicate first-run deployment
644
+ const deploymentMarker = path.join(tribeDir, '.cluster-deployed');
645
+
646
+ // Check if we've already deployed
647
+ if (fs.existsSync(deploymentMarker)) {
648
+ // Check if cluster actually exists
649
+ if (await checkClusterExists()) {
650
+ spinner.succeed('TRIBE cluster already deployed');
651
+ return true;
652
+ }
653
+ // Marker exists but cluster doesn't - remove marker and redeploy
654
+ fs.unlinkSync(deploymentMarker);
655
+ }
656
+
657
+ // Copy deployment YAML to .tribe directory
658
+ const sourceYaml = path.join(__dirname, 'tribe-deployment.yaml');
659
+ const destYaml = path.join(tribeDir, 'tribe-deployment.yaml');
660
+
661
+ if (fs.existsSync(sourceYaml)) {
662
+ fs.copyFileSync(sourceYaml, destYaml);
663
+ log.info('Copied deployment configuration');
664
+ }
665
+
666
+ // Run tribe start command with deployment YAML path
667
+ spinner.text = 'Running TRIBE cluster deployment...';
668
+ const tribePath = path.join(binDir, 'tribe');
669
+
670
+ // Set environment variable for the deployment YAML location
671
+ const env = {
672
+ ...process.env,
673
+ PATH: `${binDir}:${process.env.PATH}`,
674
+ TRIBE_DEPLOYMENT_YAML: destYaml
675
+ };
676
+
677
+ // Execute tribe start
678
+ execSync(`${tribePath} start`, {
679
+ stdio: 'pipe',
680
+ env: env
681
+ });
682
+
683
+ // Create marker file
684
+ fs.writeFileSync(deploymentMarker, new Date().toISOString());
685
+
686
+ spinner.succeed('TRIBE cluster deployed successfully');
687
+ return true;
688
+ } catch (error) {
689
+ spinner.fail('Failed to deploy TRIBE cluster');
690
+ log.error(error.message);
691
+ log.info('You can manually deploy later with: tribe start');
692
+ return false;
693
+ }
694
+ }
695
+
696
+ async function promptForClusterSetup() {
697
+ // Simple prompt without external dependencies
698
+ return new Promise((resolve) => {
699
+ console.log('\n' + chalk.bold('🚀 TRIBE Cluster Setup'));
700
+ console.log('\nWould you like to set up the TRIBE cluster now?');
701
+ console.log('This will:');
702
+ console.log(' • Start Colima with Kubernetes');
703
+ console.log(' • Deploy all TRIBE services');
704
+ console.log(' • Set up port forwarding');
705
+ console.log('\n' + chalk.yellow('Note: This requires ~2GB disk space and may take a few minutes'));
706
+
707
+ process.stdout.write('\nSet up now? [Y/n]: ');
708
+
709
+ process.stdin.resume();
710
+ process.stdin.setEncoding('utf8');
711
+ process.stdin.once('data', (data) => {
712
+ process.stdin.pause();
713
+ const answer = data.toString().trim().toLowerCase();
714
+ resolve(answer === '' || answer === 'y' || answer === 'yes');
715
+ });
716
+ });
717
+ }
718
+
719
+ async function main() {
720
+ console.log(chalk.bold.blue('\n🚀 TRIBE CLI Complete Installer\n'));
721
+
722
+ log.info(`Detected: ${platform} (${arch})`);
723
+ log.info(`Installing to: ${binDir}`);
724
+
725
+ // Update PATH first
726
+ await updatePath();
727
+
728
+ const tasks = [
729
+ { name: 'Docker CLI', fn: installDocker },
730
+ { name: 'Colima', fn: installColima },
731
+ { name: 'Lima', fn: installLima },
732
+ { name: 'KIND', fn: installKind },
733
+ { name: 'kubectl', fn: installKubectl },
734
+ { name: 'TRIBE CLI', fn: installTribeCli }
735
+ ];
736
+
737
+ let allSuccess = true;
738
+
739
+ for (const task of tasks) {
740
+ log.step(`Installing ${task.name}...`);
741
+ const success = await task.fn();
742
+ if (!success) allSuccess = false;
743
+ }
744
+
745
+ // Try to start container runtime
746
+ await startContainerRuntime();
747
+
748
+ // Verify everything
749
+ const verified = await verifyInstallation();
750
+
751
+ if (verified) {
752
+ // Check if cluster already exists
753
+ const clusterExists = await checkClusterExists();
754
+
755
+ if (!clusterExists) {
756
+ // Prompt for cluster setup
757
+ const shouldSetup = await promptForClusterSetup();
758
+
759
+ if (shouldSetup) {
760
+ console.log('');
761
+
762
+ // Start Colima with Kubernetes if on macOS
763
+ if (platform === 'darwin') {
764
+ const colimaStarted = await startColimaWithKubernetes();
765
+ if (!colimaStarted) {
766
+ log.error('Failed to start Colima. Please run manually:');
767
+ console.log(' colima start --kubernetes');
768
+ console.log(' tribe start');
769
+ process.exit(1);
770
+ }
771
+ }
772
+
773
+ // Deploy TRIBE cluster
774
+ const deployed = await deployTribeCluster();
775
+
776
+ if (deployed) {
777
+ console.log('\n' + chalk.bold.green('✨ TRIBE is ready!'));
778
+ console.log('');
779
+ log.info('Quick start:');
780
+ console.log(' tribe # Launch interactive CLI');
781
+ console.log(' tribe status # Check cluster status');
782
+ console.log(' tribe create-task # Create a new task');
783
+ console.log('');
784
+ log.info('First time? The CLI will guide you through creating your first project!');
785
+ } else {
786
+ log.warning('Cluster deployment failed, but you can try manually:');
787
+ console.log(' tribe start');
788
+ }
789
+ } else {
790
+ console.log('\n' + chalk.bold('Setup Complete!'));
791
+ log.info('You can set up the cluster later with:');
792
+ console.log(' tribe start');
793
+ }
794
+ } else {
795
+ console.log('\n' + chalk.bold.green('✨ TRIBE is ready!'));
796
+ log.success('Cluster is already running');
797
+ console.log('');
798
+ log.info('Commands:');
799
+ console.log(' tribe # Launch interactive CLI');
800
+ console.log(' tribe status # Check status');
801
+ console.log(' tribe create-task # Create a new task');
802
+ }
803
+ } else {
804
+ console.log('\n' + chalk.bold('Next Steps:'));
805
+ log.warning('Some components need attention:');
806
+ console.log('');
807
+ // Check container runtime for guidance
808
+ let runtimeWorking = false;
809
+ try {
810
+ execSync('docker info', { stdio: 'ignore' });
811
+ runtimeWorking = true;
812
+ } catch {
813
+ runtimeWorking = false;
814
+ }
815
+
816
+ if (!runtimeWorking) {
817
+ log.info('Start container runtime:');
818
+ console.log(' colima start # macOS');
819
+ console.log(' sudo systemctl start docker # Linux');
820
+ }
821
+ console.log('');
822
+ log.info('Restart your shell or run: source ~/.zshrc');
823
+ log.info('Then run: tribe start');
824
+ }
825
+ }
826
+
827
+ // Handle fetch polyfill for older Node versions
828
+ if (!global.fetch) {
829
+ global.fetch = require('node-fetch');
830
+ }
831
+
832
+ if (require.main === module) {
833
+ const args = process.argv.slice(2);
834
+
835
+ if (args.includes('--help') || args.includes('-h')) {
836
+ console.log(chalk.bold.blue('TRIBE CLI Installer\n'));
837
+ console.log('Usage: npx tribe-cli-local [options]\n');
838
+ console.log('Options:');
839
+ console.log(' --help, -h Show this help message');
840
+ console.log(' --verify Only verify existing installation');
841
+ console.log(' --dry-run Show what would be installed');
842
+ console.log('\nThis installer sets up the complete TRIBE development environment:');
843
+ console.log('• Docker CLI + Colima (macOS container runtime)');
844
+ console.log('• KIND (Kubernetes in Docker)');
845
+ console.log('• kubectl (Kubernetes CLI)');
846
+ console.log('• TRIBE CLI (Multi-agent orchestration)');
847
+ console.log('\nAfter installation:');
848
+ console.log(' tribe start # Start TRIBE cluster');
849
+ console.log(' tribe status # Check cluster status');
850
+ process.exit(0);
851
+ }
852
+
853
+ if (args.includes('--verify')) {
854
+ console.log(chalk.bold.blue('🔍 Verifying TRIBE Installation\n'));
855
+ verifyInstallation().then(success => {
856
+ process.exit(success ? 0 : 1);
857
+ });
858
+ return;
859
+ }
860
+
861
+ if (args.includes('--dry-run')) {
862
+ console.log(chalk.bold.blue('🔍 TRIBE CLI Installation Preview\n'));
863
+ log.info(`Platform: ${platform} (${arch})`);
864
+ log.info(`Install directory: ${binDir}`);
865
+ console.log('\nWould install:');
866
+ console.log('• Docker CLI (if not present)');
867
+ console.log('• Colima container runtime (macOS only)');
868
+ console.log('• Lima virtualization (macOS only)');
869
+ console.log('• KIND - Kubernetes in Docker');
870
+ console.log('• kubectl - Kubernetes CLI');
871
+ console.log('• TRIBE CLI - Multi-agent system');
872
+ process.exit(0);
873
+ }
874
+
875
+ main().catch(error => {
876
+ console.error(chalk.red('Installation failed:'), error.message);
877
+ process.exit(1);
878
+ });
879
+ }
880
+
881
+ module.exports = { main };