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