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