vibecodingmachine-cli 2026.1.29-713 ā 2026.2.20-423
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 +124 -0
- package/package.json +3 -2
- package/src/commands/agents-check.js +69 -0
- package/src/commands/auto-direct.js +930 -145
- package/src/commands/auto.js +26 -4
- package/src/commands/ide.js +2 -1
- package/src/commands/requirements.js +23 -27
- package/src/utils/auto-mode.js +4 -1
- package/src/utils/cline-js-handler.js +218 -0
- package/src/utils/config.js +22 -0
- package/src/utils/display-formatters-complete.js +229 -0
- package/src/utils/display-formatters-extracted.js +219 -0
- package/src/utils/display-formatters.js +157 -0
- package/src/utils/feedback-handler.js +143 -0
- package/src/utils/ide-detection-complete.js +126 -0
- package/src/utils/ide-detection-extracted.js +116 -0
- package/src/utils/ide-detection.js +124 -0
- package/src/utils/interactive-backup.js +5664 -0
- package/src/utils/interactive-broken.js +280 -0
- package/src/utils/interactive.js +31 -5534
- package/src/utils/provider-checker.js +410 -0
- package/src/utils/provider-manager.js +251 -0
- package/src/utils/provider-registry.js +18 -9
- package/src/utils/requirement-actions.js +884 -0
- package/src/utils/requirements-navigator.js +585 -0
- package/src/utils/rui-trui-adapter.js +311 -0
- package/src/utils/simple-trui.js +204 -0
- package/src/utils/status-helpers-extracted.js +125 -0
- package/src/utils/status-helpers.js +107 -0
- package/src/utils/trui-debug.js +261 -0
- package/src/utils/trui-feedback.js +133 -0
- package/src/utils/trui-nav-agents.js +119 -0
- package/src/utils/trui-nav-requirements.js +268 -0
- package/src/utils/trui-nav-settings.js +157 -0
- package/src/utils/trui-nav-specifications.js +139 -0
- package/src/utils/trui-navigation.js +303 -0
- package/src/utils/trui-provider-manager.js +182 -0
- package/src/utils/trui-quick-menu.js +365 -0
- package/src/utils/trui-req-actions.js +372 -0
- package/src/utils/trui-req-tree.js +534 -0
- package/src/utils/trui-specifications.js +359 -0
- package/src/utils/trui-text-editor.js +350 -0
- package/src/utils/trui-windsurf.js +336 -0
- package/src/utils/welcome-screen-extracted.js +135 -0
- package/src/utils/welcome-screen.js +134 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const inquirer = require('inquirer');
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const { t } = require('vibecodingmachine-core');
|
|
5
|
+
const pkg = require('../../package.json');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Handle feedback submission
|
|
9
|
+
*/
|
|
10
|
+
async function handleFeedbackSubmission() {
|
|
11
|
+
console.log(chalk.bold.cyan('\nš£ ' + t('interactive.feedback.title')));
|
|
12
|
+
console.log(chalk.gray(t('interactive.feedback.instructions') + '\n'));
|
|
13
|
+
|
|
14
|
+
const { getUserProfile } = require('./auth');
|
|
15
|
+
const userProfile = await getUserProfile();
|
|
16
|
+
const userEmail = userProfile ? userProfile.email : 'anonymous';
|
|
17
|
+
|
|
18
|
+
// Ask if user wants to include a screenshot
|
|
19
|
+
let includeScreenshot = false;
|
|
20
|
+
let screenshotData = null;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const { screenshot } = await inquirer.prompt([{
|
|
24
|
+
type: 'confirm',
|
|
25
|
+
name: 'screenshot',
|
|
26
|
+
message: 'Include a screenshot with your feedback?',
|
|
27
|
+
default: false
|
|
28
|
+
}]);
|
|
29
|
+
|
|
30
|
+
includeScreenshot = screenshot;
|
|
31
|
+
|
|
32
|
+
if (includeScreenshot) {
|
|
33
|
+
console.log(chalk.gray('šø Capturing screenshot...'));
|
|
34
|
+
try {
|
|
35
|
+
const screenshot = require('screenshot-desktop');
|
|
36
|
+
const imgBuffer = await screenshot({ format: 'png' });
|
|
37
|
+
|
|
38
|
+
// Convert to base64 and check size
|
|
39
|
+
const base64 = imgBuffer.toString('base64');
|
|
40
|
+
const dataUrl = `data:image/png;base64,${base64}`;
|
|
41
|
+
const size = Buffer.byteLength(dataUrl, 'utf8');
|
|
42
|
+
|
|
43
|
+
// Much stricter size limit - 100KB max
|
|
44
|
+
if (size > 100 * 1024) {
|
|
45
|
+
console.log(chalk.yellow('ā ļø Screenshot is too large, submitting feedback without screenshot'));
|
|
46
|
+
includeScreenshot = false;
|
|
47
|
+
} else {
|
|
48
|
+
screenshotData = dataUrl;
|
|
49
|
+
console.log(chalk.green(`ā
Screenshot captured (${(size / 1024).toFixed(2)} KB)`));
|
|
50
|
+
}
|
|
51
|
+
} catch (screenshotError) {
|
|
52
|
+
console.log(chalk.yellow('ā ļø Failed to capture screenshot, continuing without it'));
|
|
53
|
+
includeScreenshot = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
// User cancelled or error occurred
|
|
58
|
+
includeScreenshot = false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(chalk.gray('\n' + t('interactive.feedback.comment.instructions') + '\n'));
|
|
62
|
+
|
|
63
|
+
const commentLines = [];
|
|
64
|
+
let emptyLineCount = 0;
|
|
65
|
+
let isFirstLine = true;
|
|
66
|
+
|
|
67
|
+
while (true) {
|
|
68
|
+
try {
|
|
69
|
+
const { line } = await inquirer.prompt([{
|
|
70
|
+
type: 'input',
|
|
71
|
+
name: 'line',
|
|
72
|
+
message: isFirstLine ? t('interactive.feedback.comment') : ''
|
|
73
|
+
}]);
|
|
74
|
+
|
|
75
|
+
isFirstLine = false;
|
|
76
|
+
|
|
77
|
+
if (line.trim() === '') {
|
|
78
|
+
emptyLineCount++;
|
|
79
|
+
if (emptyLineCount >= 2) break;
|
|
80
|
+
} else {
|
|
81
|
+
emptyLineCount = 0;
|
|
82
|
+
commentLines.push(line);
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const comment = commentLines.join('\n');
|
|
90
|
+
|
|
91
|
+
if (!comment || comment.trim().length === 0) {
|
|
92
|
+
console.log(chalk.yellow('\nā ļø ' + t('interactive.feedback.cancelled')));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
console.log(chalk.gray('\n' + t('interactive.feedback.submitting') + '...'));
|
|
98
|
+
|
|
99
|
+
const auth = require('./auth');
|
|
100
|
+
const UserDatabase = require('vibecodingmachine-core/src/database/user-schema');
|
|
101
|
+
const userDb = new UserDatabase();
|
|
102
|
+
|
|
103
|
+
// Set auth token for API requests
|
|
104
|
+
const token = await auth.getAuthToken();
|
|
105
|
+
if (token) {
|
|
106
|
+
userDb.setAuthToken(token);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const feedbackData = {
|
|
110
|
+
email: userEmail,
|
|
111
|
+
comment: comment,
|
|
112
|
+
interface: 'cli',
|
|
113
|
+
version: pkg.version,
|
|
114
|
+
type: 'cli_feedback'
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (includeScreenshot && screenshotData) {
|
|
118
|
+
feedbackData.screenshot = screenshotData;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await userDb.submitFeedback(feedbackData);
|
|
122
|
+
|
|
123
|
+
console.log(chalk.green('\nā ' + t('interactive.feedback.success')));
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.log(chalk.red('\nā ' + t('interactive.feedback.error') + ': ') + error.message);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log(chalk.gray(`\n${t('interactive.press.enter.continue')}`));
|
|
129
|
+
await new Promise(resolve => {
|
|
130
|
+
const rl = readline.createInterface({
|
|
131
|
+
input: process.stdin,
|
|
132
|
+
output: process.stdout
|
|
133
|
+
});
|
|
134
|
+
rl.question('', () => {
|
|
135
|
+
rl.close();
|
|
136
|
+
resolve();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
handleFeedbackSubmission
|
|
143
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const { exec } = require('child_process');
|
|
5
|
+
const util = require('util');
|
|
6
|
+
const execAsync = util.promisify(exec);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if a VS Code extension is installed
|
|
10
|
+
* @param {string} extensionId - Extension ID to check
|
|
11
|
+
* @returns {boolean} - Whether extension is installed
|
|
12
|
+
*/
|
|
13
|
+
async function checkVSCodeExtension(extensionId) {
|
|
14
|
+
// Check if a VS Code extension is installed
|
|
15
|
+
try {
|
|
16
|
+
const { stdout } = await execAsync('code --list-extensions', { timeout: 5000 });
|
|
17
|
+
const extensions = stdout.toLowerCase().split('\n').map(e => e.trim());
|
|
18
|
+
|
|
19
|
+
// Map common extension IDs to their actual extension IDs
|
|
20
|
+
const extensionMap = {
|
|
21
|
+
'github-copilot': 'github.copilot',
|
|
22
|
+
'amazon-q': 'amazonwebservices.amazon-q-vscode'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const actualExtensionId = extensionMap[extensionId] || extensionId;
|
|
26
|
+
return extensions.includes(actualExtensionId.toLowerCase());
|
|
27
|
+
} catch (error) {
|
|
28
|
+
// If code CLI is not available or command fails, return false
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if applications or binaries exist on the system
|
|
35
|
+
* @param {string[]} names - App bundle base names (e.g., 'Cursor' -> /Applications/Cursor.app)
|
|
36
|
+
* @param {string[]} binaries - CLI binary names to check on PATH (e.g., 'code')
|
|
37
|
+
* @returns {boolean} - Whether any of the specified apps/bins are found
|
|
38
|
+
*/
|
|
39
|
+
async function checkAppOrBinary(names = [], binaries = []) {
|
|
40
|
+
// names: app bundle base names (e.g., 'Cursor' -> /Applications/Cursor.app)
|
|
41
|
+
// binaries: CLI binary names to check on PATH (e.g., 'code')
|
|
42
|
+
const platform = os.platform();
|
|
43
|
+
// Check common application directories
|
|
44
|
+
if (platform === 'darwin') {
|
|
45
|
+
const appDirs = ['/Applications', path.join(os.homedir(), 'Applications')];
|
|
46
|
+
for (const appName of names) {
|
|
47
|
+
for (const dir of appDirs) {
|
|
48
|
+
try {
|
|
49
|
+
const p = path.join(dir, `${appName}.app`);
|
|
50
|
+
if (await fs.pathExists(p)) {
|
|
51
|
+
// Ensure this is a real application bundle (has Contents/MacOS executable)
|
|
52
|
+
try {
|
|
53
|
+
const macosDir = path.join(p, 'Contents', 'MacOS');
|
|
54
|
+
const exists = await fs.pathExists(macosDir);
|
|
55
|
+
if (exists) {
|
|
56
|
+
const files = await fs.readdir(macosDir);
|
|
57
|
+
if (files && files.length > 0) {
|
|
58
|
+
// Prefer to ensure the app is usable: use spctl to assess, fallback to quarantine xattr
|
|
59
|
+
try {
|
|
60
|
+
// spctl returns non-zero on rejected/invalid apps
|
|
61
|
+
// Use async exec to avoid blocking the event loop
|
|
62
|
+
await execAsync(`spctl --assess -v "${p}"`, { timeout: 5000 });
|
|
63
|
+
|
|
64
|
+
// additionally validate codesign quickly (timeout to avoid hangs)
|
|
65
|
+
try {
|
|
66
|
+
await execAsync(`codesign -v --deep --strict "${p}"`, { timeout: 5000 });
|
|
67
|
+
return true;
|
|
68
|
+
} catch (csErr) {
|
|
69
|
+
// codesign failed or timed out ā treat as not usable/damaged
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
// spctl failed or timed out ā check if app has quarantine attribute
|
|
74
|
+
try {
|
|
75
|
+
const { stdout } = await execAsync(`xattr -p com.apple.quarantine "${p}" 2>/dev/null || true`, { encoding: 'utf8' });
|
|
76
|
+
const out = stdout.trim();
|
|
77
|
+
if (!out) {
|
|
78
|
+
// no quarantine attribute but spctl failed ā be conservative and treat as not installed
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
// If quarantine attribute exists, treat as not installed (damaged/not allowed)
|
|
82
|
+
return false;
|
|
83
|
+
} catch (e2) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {
|
|
90
|
+
// if we can't stat inside, be conservative and continue searching
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch (e) {
|
|
94
|
+
/* ignore */
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check PATH for known binaries
|
|
101
|
+
for (const bin of binaries) {
|
|
102
|
+
try {
|
|
103
|
+
await execAsync(`which ${bin}`, { timeout: 2000 });
|
|
104
|
+
return true;
|
|
105
|
+
} catch (e) {
|
|
106
|
+
/* not found */
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check common Homebrew bin locations
|
|
111
|
+
const brewPaths = ['/opt/homebrew/bin', '/usr/local/bin'];
|
|
112
|
+
for (const bin of binaries) {
|
|
113
|
+
for (const brew of brewPaths) {
|
|
114
|
+
try {
|
|
115
|
+
if (await fs.pathExists(path.join(brew, bin))) return true;
|
|
116
|
+
} catch (e) { /* ignore */ }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
checkVSCodeExtension,
|
|
125
|
+
checkAppOrBinary
|
|
126
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const { exec } = require('child_process');
|
|
5
|
+
const util = require('util');
|
|
6
|
+
const execAsync = util.promisify(exec);
|
|
7
|
+
|
|
8
|
+
async function checkVSCodeExtension(extensionId) {
|
|
9
|
+
// Check if a VS Code extension is installed
|
|
10
|
+
try {
|
|
11
|
+
const { stdout } = await execAsync('code --list-extensions', { timeout: 5000 });
|
|
12
|
+
const extensions = stdout.toLowerCase().split('\n').map(e => e.trim());
|
|
13
|
+
|
|
14
|
+
// Map common extension IDs to their actual extension IDs
|
|
15
|
+
const extensionMap = {
|
|
16
|
+
'github-copilot': 'github.copilot',
|
|
17
|
+
'amazon-q': 'amazonwebservices.amazon-q-vscode',
|
|
18
|
+
'cline': 'saoudrizwan.claude-dev'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const actualExtensionId = extensionMap[extensionId] || extensionId;
|
|
22
|
+
return extensions.includes(actualExtensionId.toLowerCase());
|
|
23
|
+
} catch (error) {
|
|
24
|
+
// If code CLI is not available or command fails, return false
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function checkAppOrBinary(names = [], binaries = []) {
|
|
30
|
+
// names: app bundle base names (e.g., 'Cursor' -> /Applications/Cursor.app)
|
|
31
|
+
// binaries: CLI binary names to check on PATH (e.g., 'code')
|
|
32
|
+
const platform = os.platform();
|
|
33
|
+
// Check common application directories
|
|
34
|
+
if (platform === 'darwin') {
|
|
35
|
+
const appDirs = ['/Applications', path.join(os.homedir(), 'Applications')];
|
|
36
|
+
for (const appName of names) {
|
|
37
|
+
for (const dir of appDirs) {
|
|
38
|
+
try {
|
|
39
|
+
const p = path.join(dir, `${appName}.app`);
|
|
40
|
+
if (await fs.pathExists(p)) {
|
|
41
|
+
// Ensure this is a real application bundle (has Contents/MacOS executable)
|
|
42
|
+
try {
|
|
43
|
+
const macosDir = path.join(p, 'Contents', 'MacOS');
|
|
44
|
+
const exists = await fs.pathExists(macosDir);
|
|
45
|
+
if (exists) {
|
|
46
|
+
const files = await fs.readdir(macosDir);
|
|
47
|
+
if (files && files.length > 0) {
|
|
48
|
+
// Prefer to ensure the app is usable: use spctl to assess, fallback to quarantine xattr
|
|
49
|
+
try {
|
|
50
|
+
// spctl returns non-zero on rejected/invalid apps
|
|
51
|
+
// Use async exec to avoid blocking the event loop
|
|
52
|
+
await execAsync(`spctl --assess -v "${p}"`, { timeout: 5000 });
|
|
53
|
+
|
|
54
|
+
// additionally validate codesign quickly (timeout to avoid hangs)
|
|
55
|
+
try {
|
|
56
|
+
await execAsync(`codesign -v --deep --strict "${p}"`, { timeout: 5000 });
|
|
57
|
+
return true;
|
|
58
|
+
} catch (csErr) {
|
|
59
|
+
// codesign failed or timed out ā treat as not usable/damaged
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
} catch (e) {
|
|
63
|
+
// spctl failed or timed out ā check if app has quarantine attribute
|
|
64
|
+
try {
|
|
65
|
+
const { stdout } = await execAsync(`xattr -p com.apple.quarantine "${p}" 2>/dev/null || true`, { encoding: 'utf8' });
|
|
66
|
+
const out = stdout.trim();
|
|
67
|
+
if (!out) {
|
|
68
|
+
// no quarantine attribute but spctl failed ā be conservative and treat as not installed
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
// If quarantine attribute exists, treat as not installed (damaged/not allowed)
|
|
72
|
+
return false;
|
|
73
|
+
} catch (e2) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (e) {
|
|
80
|
+
// if we can't stat inside, be conservative and continue searching
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch (e) {
|
|
84
|
+
/* ignore */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check PATH for known binaries
|
|
91
|
+
for (const bin of binaries) {
|
|
92
|
+
try {
|
|
93
|
+
await execAsync(`which ${bin}`, { timeout: 2000 });
|
|
94
|
+
return true;
|
|
95
|
+
} catch (e) {
|
|
96
|
+
/* not found */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check common Homebrew bin locations
|
|
101
|
+
const brewPaths = ['/opt/homebrew/bin', '/usr/local/bin'];
|
|
102
|
+
for (const bin of binaries) {
|
|
103
|
+
for (const brew of brewPaths) {
|
|
104
|
+
try {
|
|
105
|
+
if (await fs.pathExists(path.join(brew, bin))) return true;
|
|
106
|
+
} catch (e) { /* ignore */ }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
checkVSCodeExtension,
|
|
115
|
+
checkAppOrBinary
|
|
116
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const { exec } = require('child_process');
|
|
5
|
+
const util = require('util');
|
|
6
|
+
const execAsync = util.promisify(exec);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if a VS Code extension is installed
|
|
10
|
+
* @param {string} extensionId - Extension ID to check
|
|
11
|
+
* @returns {boolean} - Whether extension is installed
|
|
12
|
+
*/
|
|
13
|
+
async function checkVSCodeExtension(extensionId) {
|
|
14
|
+
try {
|
|
15
|
+
const { stdout } = await execAsync('code --list-extensions', { timeout: 5000 });
|
|
16
|
+
const extensions = stdout.toLowerCase().split('\n').map(e => e.trim());
|
|
17
|
+
|
|
18
|
+
// Map common extension IDs to their actual extension IDs
|
|
19
|
+
const extensionMap = {
|
|
20
|
+
'github-copilot': 'github.copilot',
|
|
21
|
+
'amazon-q': 'amazonwebservices.amazon-q-vscode'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const actualExtensionId = extensionMap[extensionId] || extensionId;
|
|
25
|
+
return extensions.includes(actualExtensionId.toLowerCase());
|
|
26
|
+
} catch (error) {
|
|
27
|
+
// If code CLI is not available or command fails, return false
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if applications or binaries exist on the system
|
|
34
|
+
* @param {string[]} names - App bundle base names (e.g., 'Cursor' -> /Applications/Cursor.app)
|
|
35
|
+
* @param {string[]} binaries - CLI binary names to check on PATH (e.g., 'code')
|
|
36
|
+
* @returns {boolean} - Whether any of the specified apps/bins are found
|
|
37
|
+
*/
|
|
38
|
+
async function checkAppOrBinary(names = [], binaries = []) {
|
|
39
|
+
const platform = os.platform();
|
|
40
|
+
|
|
41
|
+
// Check common application directories
|
|
42
|
+
if (platform === 'darwin') {
|
|
43
|
+
const appDirs = ['/Applications', path.join(os.homedir(), 'Applications')];
|
|
44
|
+
for (const appName of names) {
|
|
45
|
+
for (const dir of appDirs) {
|
|
46
|
+
try {
|
|
47
|
+
const p = path.join(dir, `${appName}.app`);
|
|
48
|
+
if (await fs.pathExists(p)) {
|
|
49
|
+
// Ensure this is a real application bundle (has Contents/MacOS executable)
|
|
50
|
+
try {
|
|
51
|
+
const macosDir = path.join(p, 'Contents', 'MacOS');
|
|
52
|
+
const exists = await fs.pathExists(macosDir);
|
|
53
|
+
if (exists) {
|
|
54
|
+
const files = await fs.readdir(macosDir);
|
|
55
|
+
if (files && files.length > 0) {
|
|
56
|
+
// Prefer to ensure the app is usable: use spctl to assess, fallback to quarantine xattr
|
|
57
|
+
try {
|
|
58
|
+
// spctl returns non-zero on rejected/invalid apps
|
|
59
|
+
// Use async exec to avoid blocking the event loop
|
|
60
|
+
await execAsync(`spctl --assess -v "${p}"`, { timeout: 5000 });
|
|
61
|
+
|
|
62
|
+
// additionally validate codesign quickly (timeout to avoid hangs)
|
|
63
|
+
try {
|
|
64
|
+
await execAsync(`codesign -v --deep --strict "${p}"`, { timeout: 5000 });
|
|
65
|
+
return true;
|
|
66
|
+
} catch (csErr) {
|
|
67
|
+
// codesign failed or timed out ā treat as not usable/damaged
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
} catch (e) {
|
|
71
|
+
// spctl failed or timed out ā check if app has quarantine attribute
|
|
72
|
+
try {
|
|
73
|
+
const { stdout } = await execAsync(`xattr -p com.apple.quarantine "${p}" 2>/dev/null || true`, { encoding: 'utf8' });
|
|
74
|
+
const out = stdout.trim();
|
|
75
|
+
if (!out) {
|
|
76
|
+
// no quarantine attribute but spctl failed ā be conservative and treat as not installed
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
// If quarantine attribute exists, treat as not installed (damaged/not allowed)
|
|
80
|
+
return false;
|
|
81
|
+
} catch (e2) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch (e) {
|
|
88
|
+
// if we can't stat inside, be conservative and continue searching
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch (e) {
|
|
92
|
+
/* ignore */
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check PATH for known binaries
|
|
99
|
+
for (const bin of binaries) {
|
|
100
|
+
try {
|
|
101
|
+
await execAsync(`which ${bin}`, { timeout: 2000 });
|
|
102
|
+
return true;
|
|
103
|
+
} catch (e) {
|
|
104
|
+
/* not found */
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check common Homebrew bin locations
|
|
109
|
+
const brewPaths = ['/opt/homebrew/bin', '/usr/local/bin'];
|
|
110
|
+
for (const bin of binaries) {
|
|
111
|
+
for (const brew of brewPaths) {
|
|
112
|
+
try {
|
|
113
|
+
if (await fs.pathExists(path.join(brew, bin))) return true;
|
|
114
|
+
} catch (e) { /* ignore */ }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
checkVSCodeExtension,
|
|
123
|
+
checkAppOrBinary
|
|
124
|
+
};
|