vibecodingmachine-cli 2025.12.6-1702 → 2025.12.22-2230
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/bin/vibecodingmachine.js +209 -8
- package/package.json +2 -2
- package/reproduce_issue.js +160 -0
- package/src/commands/auth.js +0 -1
- package/src/commands/auto-direct.js +55 -40
- package/src/commands/auto.js +154 -57
- package/src/commands/computers.js +4 -4
- package/src/commands/repo.js +0 -1
- package/src/commands/requirements-remote.js +10 -6
- package/src/commands/requirements.js +29 -3
- package/src/commands/status.js +0 -1
- package/src/commands/sync.js +4 -4
- package/src/utils/agent-selector.js +50 -0
- package/src/utils/antigravity-installer.js +212 -0
- package/src/utils/antigravity-js-handler.js +60 -0
- package/src/utils/asset-cleanup.js +0 -1
- package/src/utils/auth.js +149 -2
- package/src/utils/auto-mode-ansi-ui.js +0 -1
- package/src/utils/auto-mode-simple-ui.js +1 -1
- package/src/utils/compliance-check.js +166 -0
- package/src/utils/config.js +27 -1
- package/src/utils/copy-with-progress.js +167 -0
- package/src/utils/download-with-progress.js +84 -0
- package/src/utils/first-run.js +185 -68
- package/src/utils/interactive.js +259 -263
- package/src/utils/kiro-installer.js +56 -24
- package/src/utils/persistent-header.js +1 -3
- package/src/utils/provider-registry.js +5 -4
- package/src/utils/user-tracking.js +300 -0
- package/tests/requirements-navigator-buildtree-await.test.js +28 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
const { spawn, execSync } = require('child_process');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
function progressBar(percent, width) {
|
|
6
|
+
const fill = Math.round((percent / 100) * width);
|
|
7
|
+
return '█'.repeat(fill) + '-'.repeat(Math.max(0, width - fill));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function formatEta(sec) {
|
|
11
|
+
if (!isFinite(sec) || sec === null) return '--:--';
|
|
12
|
+
const s = Math.max(0, Math.round(sec));
|
|
13
|
+
const m = Math.floor(s / 60);
|
|
14
|
+
const ss = s % 60;
|
|
15
|
+
return `${m}:${ss.toString().padStart(2, '0')}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function tryRsync(src, dest, spinner, timeoutMs = 5 * 60 * 1000) {
|
|
19
|
+
try {
|
|
20
|
+
execSync('which rsync', { stdio: 'ignore' });
|
|
21
|
+
} catch (e) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
// choose progress option based on rsync version
|
|
25
|
+
let rsyncArgs = ['-a'];
|
|
26
|
+
try {
|
|
27
|
+
const verOut = execSync('rsync --version', { encoding: 'utf8', timeout: 2000 });
|
|
28
|
+
const m = verOut.match(/version\s+(\d+)\.(\d+)/i);
|
|
29
|
+
if (m) {
|
|
30
|
+
const major = Number(m[1]);
|
|
31
|
+
const minor = Number(m[2] || 0);
|
|
32
|
+
if (major > 3 || (major === 3 && minor >= 1)) {
|
|
33
|
+
rsyncArgs.push('--info=progress2');
|
|
34
|
+
} else {
|
|
35
|
+
rsyncArgs.push('--progress');
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
rsyncArgs.push('--progress');
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
rsyncArgs.push('--progress');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return await new Promise((resolve) => {
|
|
45
|
+
const rsync = spawn('rsync', rsyncArgs.concat([src + '/', dest]), { stdio: 'inherit' });
|
|
46
|
+
let finished = false;
|
|
47
|
+
const to = setTimeout(() => {
|
|
48
|
+
if (!finished) {
|
|
49
|
+
try { rsync.kill('SIGINT'); } catch (e) { /* ignore */ }
|
|
50
|
+
resolve(false);
|
|
51
|
+
}
|
|
52
|
+
}, timeoutMs);
|
|
53
|
+
rsync.on('close', (code) => { finished = true; clearTimeout(to); resolve(code === 0); });
|
|
54
|
+
rsync.on('error', () => { finished = true; clearTimeout(to); resolve(false); });
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function tryDitto(src, dest, spinner, timeoutMs = 5 * 60 * 1000) {
|
|
59
|
+
try {
|
|
60
|
+
await fs.ensureDir(path.dirname(dest));
|
|
61
|
+
return await new Promise((resolve) => {
|
|
62
|
+
const ditto = spawn('ditto', ['-v', src, dest], { stdio: 'inherit' });
|
|
63
|
+
let finished = false;
|
|
64
|
+
const to = setTimeout(() => {
|
|
65
|
+
if (!finished) {
|
|
66
|
+
try { ditto.kill('SIGINT'); } catch (e) { /* ignore */ }
|
|
67
|
+
resolve(false);
|
|
68
|
+
}
|
|
69
|
+
}, timeoutMs);
|
|
70
|
+
ditto.on('close', (code) => { finished = true; clearTimeout(to); resolve(code === 0); });
|
|
71
|
+
ditto.on('error', () => { finished = true; clearTimeout(to); resolve(false); });
|
|
72
|
+
});
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function nodeStreamCopy(src, dest, _spinner) {
|
|
79
|
+
// Determine total size (attempt du -sk fallback to recursive stat)
|
|
80
|
+
let total = 0;
|
|
81
|
+
try {
|
|
82
|
+
const out = execSync(`du -sk "${src}" | cut -f1`, { encoding: 'utf8' }).trim();
|
|
83
|
+
total = Number(out) * 1024;
|
|
84
|
+
} catch (e) {
|
|
85
|
+
// fallback: sum file sizes via traversal
|
|
86
|
+
await (async function walk(p) {
|
|
87
|
+
const entries = await fs.readdir(p);
|
|
88
|
+
for (const e of entries) {
|
|
89
|
+
const full = path.join(p, e);
|
|
90
|
+
const stat = await fs.stat(full);
|
|
91
|
+
if (stat.isDirectory()) await walk(full);
|
|
92
|
+
else total += stat.size;
|
|
93
|
+
}
|
|
94
|
+
})(src);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let copied = 0;
|
|
98
|
+
const start = Date.now();
|
|
99
|
+
const width = 30;
|
|
100
|
+
|
|
101
|
+
async function copyEntry(srcPath, dstPath) {
|
|
102
|
+
const stat = await fs.stat(srcPath);
|
|
103
|
+
if (stat.isDirectory()) {
|
|
104
|
+
await fs.ensureDir(dstPath);
|
|
105
|
+
const entries = await fs.readdir(srcPath);
|
|
106
|
+
for (const e of entries) {
|
|
107
|
+
await copyEntry(path.join(srcPath, e), path.join(dstPath, e));
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
await fs.ensureDir(path.dirname(dstPath));
|
|
111
|
+
await new Promise((resolve, reject) => {
|
|
112
|
+
const rs = fs.createReadStream(srcPath);
|
|
113
|
+
const ws = fs.createWriteStream(dstPath);
|
|
114
|
+
rs.on('data', (chunk) => {
|
|
115
|
+
copied += chunk.length;
|
|
116
|
+
if (total) {
|
|
117
|
+
const percent = Math.round((copied / total) * 100);
|
|
118
|
+
const mbCopied = (copied / (1024 * 1024)).toFixed(1);
|
|
119
|
+
const mbTotal = (total / (1024 * 1024)).toFixed(1);
|
|
120
|
+
const elapsed = Math.max(0.001, (Date.now() - start) / 1000);
|
|
121
|
+
const speed = copied / elapsed;
|
|
122
|
+
const eta = formatEta((total - copied) / (speed || 1));
|
|
123
|
+
const bar = progressBar(percent, width);
|
|
124
|
+
process.stdout.write(`\r\x1b[2K[${bar}] ${percent}% ${mbCopied}MB / ${mbTotal}MB ETA: ${eta}`);
|
|
125
|
+
} else {
|
|
126
|
+
process.stdout.write(`\r\x1b[2KCopying ${ (copied/(1024*1024)).toFixed(1) } MB`);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
rs.on('error', reject);
|
|
130
|
+
ws.on('error', reject);
|
|
131
|
+
ws.on('close', resolve);
|
|
132
|
+
rs.pipe(ws);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await copyEntry(src, dest);
|
|
138
|
+
process.stdout.write('\n');
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function copyAppWithProgress(src, dest, opts = {}) {
|
|
143
|
+
const spinner = (opts && opts.spinner) || { start: () => {}, stop: () => {}, fail: () => {}, succeed: () => {} };
|
|
144
|
+
|
|
145
|
+
// Try rsync first
|
|
146
|
+
spinner.stop && spinner.stop();
|
|
147
|
+
console.log(`Copying ${path.basename(src)} -> ${dest} (attempting rsync...)`);
|
|
148
|
+
const okRsync = await tryRsync(src, dest, spinner);
|
|
149
|
+
if (okRsync) return true;
|
|
150
|
+
|
|
151
|
+
// Try ditto (macOS)
|
|
152
|
+
console.log('rsync failed or not available — trying ditto...');
|
|
153
|
+
const okDitto = await tryDitto(src, dest, spinner);
|
|
154
|
+
if (okDitto) return true;
|
|
155
|
+
|
|
156
|
+
// Fallback to Node streaming copy with progress
|
|
157
|
+
console.log('Falling back to node-stream copy with progress...');
|
|
158
|
+
try {
|
|
159
|
+
await nodeStreamCopy(src, dest, spinner);
|
|
160
|
+
return true;
|
|
161
|
+
} catch (e) {
|
|
162
|
+
console.error('node-stream copy failed:', e.message || e);
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = { copyAppWithProgress };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const ora = require('ora');
|
|
3
|
+
|
|
4
|
+
async function downloadWithProgress(url, dest, opts = {}) {
|
|
5
|
+
const fetch = require('node-fetch');
|
|
6
|
+
const spinner = opts.spinner || ora();
|
|
7
|
+
const label = opts.label || 'Downloading...';
|
|
8
|
+
|
|
9
|
+
spinner.start(label);
|
|
10
|
+
|
|
11
|
+
const res = await fetch(url);
|
|
12
|
+
if (!res.ok) {
|
|
13
|
+
spinner.fail(`Download failed: ${res.status} ${res.statusText}`);
|
|
14
|
+
throw new Error(`Failed to download ${url}: ${res.status}`);
|
|
15
|
+
}
|
|
16
|
+
// Stop the ora spinner so we can write an in-place progress line without conflicts
|
|
17
|
+
try { spinner.stop(); } catch (e) { /* ignore */ }
|
|
18
|
+
// Print initial progress line so user sees immediate feedback
|
|
19
|
+
try { process.stdout.write('\r\x1b[2KDownloading: 0.0 MB'); } catch (e) { /* ignore */ }
|
|
20
|
+
|
|
21
|
+
const total = Number(res.headers.get('content-length')) || 0;
|
|
22
|
+
const fileStream = fs.createWriteStream(dest);
|
|
23
|
+
|
|
24
|
+
return await new Promise((resolve, reject) => {
|
|
25
|
+
let downloaded = 0;
|
|
26
|
+
const start = Date.now();
|
|
27
|
+
let lastPercent = -1;
|
|
28
|
+
|
|
29
|
+
res.body.on('data', (chunk) => {
|
|
30
|
+
downloaded += chunk.length;
|
|
31
|
+
if (total) {
|
|
32
|
+
const percent = Math.round((downloaded / total) * 100);
|
|
33
|
+
if (percent !== lastPercent) {
|
|
34
|
+
lastPercent = percent;
|
|
35
|
+
const mbDownloaded = (downloaded / (1024 * 1024)).toFixed(1);
|
|
36
|
+
const mbTotal = (total / (1024 * 1024)).toFixed(1);
|
|
37
|
+
const elapsed = Math.max(0.001, (Date.now() - start) / 1000);
|
|
38
|
+
const speed = downloaded / elapsed; // bytes/sec
|
|
39
|
+
const etaSec = (total - downloaded) / (speed || 1);
|
|
40
|
+
const eta = formatEta(etaSec);
|
|
41
|
+
const bar = progressBar(percent, 30);
|
|
42
|
+
process.stdout.write(`\r\x1b[2K[${bar}] ${percent}% ${mbDownloaded}MB / ${mbTotal}MB ETA: ${eta}`);
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
const mbDownloaded = (downloaded / (1024 * 1024)).toFixed(1);
|
|
46
|
+
process.stdout.write(`\r\x1b[2K${label} ${mbDownloaded} MB`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
res.body.on('error', (err) => {
|
|
51
|
+
spinner.fail('Download error');
|
|
52
|
+
reject(err);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
fileStream.on('error', (err) => {
|
|
56
|
+
spinner.fail('File write error');
|
|
57
|
+
reject(err);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
fileStream.on('finish', () => {
|
|
61
|
+
process.stdout.write('\n');
|
|
62
|
+
spinner.succeed('Download complete');
|
|
63
|
+
resolve();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Pipe the response body to file
|
|
67
|
+
res.body.pipe(fileStream);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function progressBar(percent, width) {
|
|
72
|
+
const fill = Math.round((percent / 100) * width);
|
|
73
|
+
return '█'.repeat(fill) + '-'.repeat(Math.max(0, width - fill));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatEta(sec) {
|
|
77
|
+
if (!isFinite(sec) || sec === null) return '--:--';
|
|
78
|
+
const s = Math.max(0, Math.round(sec));
|
|
79
|
+
const m = Math.floor(s / 60);
|
|
80
|
+
const ss = s % 60;
|
|
81
|
+
return `${m}:${ss.toString().padStart(2, '0')}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { downloadWithProgress };
|
package/src/utils/first-run.js
CHANGED
|
@@ -5,7 +5,90 @@ const fs = require('fs-extra');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const { getProviderDefinitions, saveProviderPreferences, getDefaultProviderOrder } = require('./provider-registry');
|
|
8
|
-
const { isKiroInstalled
|
|
8
|
+
const { isKiroInstalled } = require('./kiro-installer');
|
|
9
|
+
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
async function checkAppOrBinary(names = [], binaries = []) {
|
|
13
|
+
// names: app bundle base names (e.g., 'Cursor' -> /Applications/Cursor.app)
|
|
14
|
+
// binaries: CLI binary names to check on PATH (e.g., 'code')
|
|
15
|
+
const platform = os.platform();
|
|
16
|
+
// Check common application directories
|
|
17
|
+
if (platform === 'darwin') {
|
|
18
|
+
const appDirs = ['/Applications', path.join(os.homedir(), 'Applications')];
|
|
19
|
+
for (const appName of names) {
|
|
20
|
+
for (const dir of appDirs) {
|
|
21
|
+
try {
|
|
22
|
+
const p = path.join(dir, `${appName}.app`);
|
|
23
|
+
if (await fs.pathExists(p)) {
|
|
24
|
+
// Ensure this is a real application bundle (has Contents/MacOS executable)
|
|
25
|
+
try {
|
|
26
|
+
const macosDir = path.join(p, 'Contents', 'MacOS');
|
|
27
|
+
const exists = await fs.pathExists(macosDir);
|
|
28
|
+
if (exists) {
|
|
29
|
+
const files = await fs.readdir(macosDir);
|
|
30
|
+
if (files && files.length > 0) {
|
|
31
|
+
// Prefer to ensure the app is usable: use spctl to assess, fallback to quarantine xattr
|
|
32
|
+
try {
|
|
33
|
+
// spctl returns non-zero on rejected/invalid apps
|
|
34
|
+
execSync(`spctl --assess -v "${p}"`, { stdio: 'ignore', timeout: 5000 });
|
|
35
|
+
// additionally validate codesign quickly (timeout to avoid hangs)
|
|
36
|
+
try {
|
|
37
|
+
execSync(`codesign -v --deep --strict "${p}"`, { stdio: 'ignore', timeout: 5000 });
|
|
38
|
+
return true;
|
|
39
|
+
} catch (csErr) {
|
|
40
|
+
// codesign failed or timed out — treat as not usable/damaged
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// spctl failed or timed out — check if app has quarantine attribute
|
|
45
|
+
try {
|
|
46
|
+
const out = execSync(`xattr -p com.apple.quarantine "${p}" 2>/dev/null || true`, { encoding: 'utf8' }).trim();
|
|
47
|
+
if (!out) {
|
|
48
|
+
// no quarantine attribute but spctl failed — be conservative and treat as not installed
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
// If quarantine attribute exists, treat as not installed (damaged/not allowed)
|
|
52
|
+
return false;
|
|
53
|
+
} catch (e2) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch (e) {
|
|
60
|
+
// if we can't stat inside, be conservative and continue searching
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (e) {
|
|
64
|
+
/* ignore */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check PATH for known binaries
|
|
71
|
+
for (const bin of binaries) {
|
|
72
|
+
try {
|
|
73
|
+
execSync(`which ${bin}`, { stdio: 'ignore' });
|
|
74
|
+
return true;
|
|
75
|
+
} catch (e) {
|
|
76
|
+
/* not found */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check common Homebrew bin locations
|
|
81
|
+
const brewPaths = ['/opt/homebrew/bin', '/usr/local/bin'];
|
|
82
|
+
for (const bin of binaries) {
|
|
83
|
+
for (const brew of brewPaths) {
|
|
84
|
+
try {
|
|
85
|
+
if (await fs.pathExists(path.join(brew, bin))) return true;
|
|
86
|
+
} catch (e) { /* ignore */ }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
9
92
|
|
|
10
93
|
async function checkFirstRun() {
|
|
11
94
|
const configDir = path.join(os.homedir(), '.vibecodingmachine');
|
|
@@ -85,7 +168,6 @@ async function checkFirstRun() {
|
|
|
85
168
|
// Simple simulation of detection for standard apps on macOS
|
|
86
169
|
// In a real scenario, we might check other paths or registry on Windows
|
|
87
170
|
const platform = os.platform();
|
|
88
|
-
const getAppPath = (name) => platform === 'darwin' ? `/Applications/${name}.app` : null;
|
|
89
171
|
|
|
90
172
|
for (const ide of ideDefinitions) {
|
|
91
173
|
let installed = false;
|
|
@@ -93,15 +175,17 @@ async function checkFirstRun() {
|
|
|
93
175
|
if (ide.id === 'kiro') {
|
|
94
176
|
installed = isKiroInstalled();
|
|
95
177
|
} else if (ide.id === 'cursor') {
|
|
96
|
-
|
|
178
|
+
// Cursor: check app bundle and 'cursor' binary
|
|
179
|
+
installed = await checkAppOrBinary(['Cursor'], ['cursor']);
|
|
97
180
|
} else if (ide.id === 'windsurf') {
|
|
98
|
-
|
|
181
|
+
// Windsurf: check app bundle and common binary
|
|
182
|
+
installed = await checkAppOrBinary(['Windsurf'], ['windsurf']);
|
|
99
183
|
} else if (ide.id === 'vscode') {
|
|
100
|
-
|
|
184
|
+
// VS Code: check app bundle and 'code' CLI
|
|
185
|
+
installed = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
|
|
101
186
|
} else if (ide.id === 'antigravity') {
|
|
102
|
-
// Antigravity
|
|
103
|
-
|
|
104
|
-
installed = true; // Assume available
|
|
187
|
+
// Antigravity: check app bundle and 'antigravity' binary
|
|
188
|
+
installed = await checkAppOrBinary(['Antigravity'], ['antigravity']);
|
|
105
189
|
}
|
|
106
190
|
|
|
107
191
|
if (installed) {
|
|
@@ -140,84 +224,117 @@ async function checkFirstRun() {
|
|
|
140
224
|
selectedIDEs = response.selectedIDEs;
|
|
141
225
|
}
|
|
142
226
|
|
|
143
|
-
// 4. Handle Installations
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
227
|
+
// 4. Handle Installations (Generic)
|
|
228
|
+
// For any selected IDE that wasn't detected, try to invoke a matching installer module
|
|
229
|
+
// Installer modules are expected to live next to this file as `<id>-installer.js` and
|
|
230
|
+
// export an installation function like `install<IdPascal>()` or `install`.
|
|
231
|
+
for (const ideId of selectedIDEs) {
|
|
232
|
+
// determine if already installed using existing checks
|
|
233
|
+
let alreadyInstalled = false;
|
|
234
|
+
try {
|
|
235
|
+
if (ideId === 'kiro') {
|
|
236
|
+
alreadyInstalled = isKiroInstalled() || await checkAppOrBinary(['AWS Kiro', 'Kiro'], ['kiro']);
|
|
237
|
+
} else if (ideId === 'cursor') {
|
|
238
|
+
alreadyInstalled = await checkAppOrBinary(['Cursor'], ['cursor']);
|
|
239
|
+
} else if (ideId === 'windsurf') {
|
|
240
|
+
alreadyInstalled = await checkAppOrBinary(['Windsurf'], ['windsurf']);
|
|
241
|
+
} else if (ideId === 'vscode') {
|
|
242
|
+
alreadyInstalled = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
|
|
243
|
+
} else if (ideId === 'antigravity') {
|
|
244
|
+
alreadyInstalled = await checkAppOrBinary(['Antigravity'], ['antigravity']);
|
|
153
245
|
} else {
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
chalk.cyan('You must sign in for the CLI to control the IDE.'),
|
|
162
|
-
{
|
|
163
|
-
padding: 1,
|
|
164
|
-
margin: 1,
|
|
165
|
-
borderStyle: 'round',
|
|
166
|
-
borderColor: 'yellow'
|
|
167
|
-
}
|
|
168
|
-
));
|
|
246
|
+
// default fallback: try binary name same as id
|
|
247
|
+
alreadyInstalled = await checkAppOrBinary([], [ideId]);
|
|
248
|
+
}
|
|
249
|
+
} catch (e) {
|
|
250
|
+
// detection error: assume not installed
|
|
251
|
+
alreadyInstalled = false;
|
|
252
|
+
}
|
|
169
253
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
254
|
+
if (alreadyInstalled) continue;
|
|
255
|
+
|
|
256
|
+
// Attempt to locate and run an installer module for this IDE
|
|
257
|
+
let installerModule = null;
|
|
258
|
+
const tryPaths = [`./${ideId}-installer`, `../utils/${ideId}-installer`];
|
|
259
|
+
for (const p of tryPaths) {
|
|
260
|
+
try {
|
|
261
|
+
// require may throw if module doesn't exist
|
|
262
|
+
installerModule = require(p);
|
|
263
|
+
break;
|
|
264
|
+
} catch (e) {
|
|
265
|
+
// ignore and try next
|
|
266
|
+
}
|
|
267
|
+
}
|
|
175
268
|
|
|
176
|
-
|
|
177
|
-
|
|
269
|
+
if (!installerModule) {
|
|
270
|
+
console.log(chalk.gray(`No installer module found for ${ideId}, skipping automated install.`));
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
178
273
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
274
|
+
// Resolve a callable install function from the module
|
|
275
|
+
const pascal = ideId.split(/[-_]/).map(s => s[0].toUpperCase() + s.slice(1)).join('');
|
|
276
|
+
const candidateNames = [`install${pascal}`, 'install', pascal, 'default'];
|
|
277
|
+
let installerFn = null;
|
|
278
|
+
for (const name of candidateNames) {
|
|
279
|
+
if (name === 'default' && typeof installerModule === 'function') {
|
|
280
|
+
installerFn = installerModule;
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
if (installerModule && typeof installerModule[name] === 'function') {
|
|
284
|
+
installerFn = installerModule[name];
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!installerFn) {
|
|
290
|
+
console.log(chalk.gray(`Installer found for ${ideId} but no callable export (tried ${candidateNames.join(', ')}).`));
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
194
293
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
294
|
+
console.log(chalk.cyan(`\n🔧 Installing ${ideId}...`));
|
|
295
|
+
try {
|
|
296
|
+
const ok = await installerFn();
|
|
297
|
+
if (!ok) {
|
|
298
|
+
console.log(chalk.yellow(`\n⚠️ ${ideId} installation failed or was skipped.`));
|
|
299
|
+
} else {
|
|
300
|
+
console.log(chalk.green(`\n✅ ${ideId} installation complete.`));
|
|
200
301
|
}
|
|
302
|
+
} catch (err) {
|
|
303
|
+
console.log(chalk.red(`${ideId} installation error:`), err.message || err);
|
|
201
304
|
}
|
|
202
305
|
}
|
|
203
306
|
|
|
204
307
|
// 5. Configure Preferences
|
|
205
|
-
//
|
|
308
|
+
// Re-detect IDEs to ensure we only enable actually installed ones
|
|
309
|
+
const reDetected = [];
|
|
310
|
+
for (const ideDef of ideDefinitions) {
|
|
311
|
+
let installedNow = false;
|
|
312
|
+
if (ideDef.id === 'kiro') {
|
|
313
|
+
installedNow = isKiroInstalled() || await checkAppOrBinary(['AWS Kiro', 'Kiro'], ['kiro']);
|
|
314
|
+
} else if (ideDef.id === 'cursor') {
|
|
315
|
+
installedNow = await checkAppOrBinary(['Cursor'], ['cursor']);
|
|
316
|
+
} else if (ideDef.id === 'windsurf') {
|
|
317
|
+
installedNow = await checkAppOrBinary(['Windsurf'], ['windsurf']);
|
|
318
|
+
} else if (ideDef.id === 'vscode') {
|
|
319
|
+
installedNow = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
|
|
320
|
+
} else if (ideDef.id === 'antigravity') {
|
|
321
|
+
installedNow = await checkAppOrBinary(['Antigravity'], ['antigravity']);
|
|
322
|
+
}
|
|
323
|
+
if (installedNow) reDetected.push(ideDef.id);
|
|
324
|
+
}
|
|
325
|
+
|
|
206
326
|
const defaultOrder = getDefaultProviderOrder();
|
|
207
327
|
const enabledMap = {};
|
|
208
|
-
|
|
209
|
-
defaultOrder.forEach(id => {
|
|
210
|
-
// Enable if it was detected OR selected
|
|
211
|
-
// For LLMs (not 'ide' type), keep enabled by default
|
|
328
|
+
for (const id of defaultOrder) {
|
|
212
329
|
const def = definitions.find(d => d.id === id);
|
|
213
330
|
if (def && def.type === 'ide') {
|
|
214
|
-
const
|
|
331
|
+
const isDetectedNow = reDetected.includes(id);
|
|
215
332
|
const isSelected = selectedIDEs.includes(id);
|
|
216
|
-
enabledMap[id] =
|
|
333
|
+
enabledMap[id] = isDetectedNow || isSelected;
|
|
217
334
|
} else {
|
|
218
335
|
enabledMap[id] = true; // Keep LLMs enabled by default
|
|
219
336
|
}
|
|
220
|
-
}
|
|
337
|
+
}
|
|
221
338
|
|
|
222
339
|
// Save initial preferences. provider-registry.js usually saves to ~/.config/vibecodingmachine/config.json
|
|
223
340
|
// But here we are checking ~/.vibecodingmachine.
|