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