vibecodingmachine-cli 2025.12.6-1702 → 2025.12.24-2348

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.
@@ -5,7 +5,91 @@ 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, installKiro } = require('./kiro-installer');
8
+ const { isKiroInstalled } = require('./kiro-installer');
9
+ const { t } = require('vibecodingmachine-core');
10
+
11
+ const { execSync } = require('child_process');
12
+
13
+ async function checkAppOrBinary(names = [], binaries = []) {
14
+ // names: app bundle base names (e.g., 'Cursor' -> /Applications/Cursor.app)
15
+ // binaries: CLI binary names to check on PATH (e.g., 'code')
16
+ const platform = os.platform();
17
+ // Check common application directories
18
+ if (platform === 'darwin') {
19
+ const appDirs = ['/Applications', path.join(os.homedir(), 'Applications')];
20
+ for (const appName of names) {
21
+ for (const dir of appDirs) {
22
+ try {
23
+ const p = path.join(dir, `${appName}.app`);
24
+ if (await fs.pathExists(p)) {
25
+ // Ensure this is a real application bundle (has Contents/MacOS executable)
26
+ try {
27
+ const macosDir = path.join(p, 'Contents', 'MacOS');
28
+ const exists = await fs.pathExists(macosDir);
29
+ if (exists) {
30
+ const files = await fs.readdir(macosDir);
31
+ if (files && files.length > 0) {
32
+ // Prefer to ensure the app is usable: use spctl to assess, fallback to quarantine xattr
33
+ try {
34
+ // spctl returns non-zero on rejected/invalid apps
35
+ execSync(`spctl --assess -v "${p}"`, { stdio: 'ignore', timeout: 5000 });
36
+ // additionally validate codesign quickly (timeout to avoid hangs)
37
+ try {
38
+ execSync(`codesign -v --deep --strict "${p}"`, { stdio: 'ignore', timeout: 5000 });
39
+ return true;
40
+ } catch (csErr) {
41
+ // codesign failed or timed out — treat as not usable/damaged
42
+ return false;
43
+ }
44
+ } catch (e) {
45
+ // spctl failed or timed out — check if app has quarantine attribute
46
+ try {
47
+ const out = execSync(`xattr -p com.apple.quarantine "${p}" 2>/dev/null || true`, { encoding: 'utf8' }).trim();
48
+ if (!out) {
49
+ // no quarantine attribute but spctl failed — be conservative and treat as not installed
50
+ return false;
51
+ }
52
+ // If quarantine attribute exists, treat as not installed (damaged/not allowed)
53
+ return false;
54
+ } catch (e2) {
55
+ return false;
56
+ }
57
+ }
58
+ }
59
+ }
60
+ } catch (e) {
61
+ // if we can't stat inside, be conservative and continue searching
62
+ }
63
+ }
64
+ } catch (e) {
65
+ /* ignore */
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ // Check PATH for known binaries
72
+ for (const bin of binaries) {
73
+ try {
74
+ execSync(`which ${bin}`, { stdio: 'ignore' });
75
+ return true;
76
+ } catch (e) {
77
+ /* not found */
78
+ }
79
+ }
80
+
81
+ // Check common Homebrew bin locations
82
+ const brewPaths = ['/opt/homebrew/bin', '/usr/local/bin'];
83
+ for (const bin of binaries) {
84
+ for (const brew of brewPaths) {
85
+ try {
86
+ if (await fs.pathExists(path.join(brew, bin))) return true;
87
+ } catch (e) { /* ignore */ }
88
+ }
89
+ }
90
+
91
+ return false;
92
+ }
9
93
 
10
94
  async function checkFirstRun() {
11
95
  const configDir = path.join(os.homedir(), '.vibecodingmachine');
@@ -68,7 +152,7 @@ async function checkFirstRun() {
68
152
  await inquirer.prompt([{
69
153
  type: 'input',
70
154
  name: 'continue',
71
- message: 'Press Enter to continue...'
155
+ message: t('interactive.press.enter.continue')
72
156
  }]);
73
157
  }
74
158
 
@@ -85,7 +169,6 @@ async function checkFirstRun() {
85
169
  // Simple simulation of detection for standard apps on macOS
86
170
  // In a real scenario, we might check other paths or registry on Windows
87
171
  const platform = os.platform();
88
- const getAppPath = (name) => platform === 'darwin' ? `/Applications/${name}.app` : null;
89
172
 
90
173
  for (const ide of ideDefinitions) {
91
174
  let installed = false;
@@ -93,15 +176,17 @@ async function checkFirstRun() {
93
176
  if (ide.id === 'kiro') {
94
177
  installed = isKiroInstalled();
95
178
  } else if (ide.id === 'cursor') {
96
- if (platform === 'darwin') installed = await fs.pathExists(getAppPath('Cursor'));
179
+ // Cursor: check app bundle and 'cursor' binary
180
+ installed = await checkAppOrBinary(['Cursor'], ['cursor']);
97
181
  } else if (ide.id === 'windsurf') {
98
- if (platform === 'darwin') installed = await fs.pathExists(getAppPath('Windsurf'));
182
+ // Windsurf: check app bundle and common binary
183
+ installed = await checkAppOrBinary(['Windsurf'], ['windsurf']);
99
184
  } else if (ide.id === 'vscode') {
100
- if (platform === 'darwin') installed = await fs.pathExists(getAppPath('Visual Studio Code'));
185
+ // VS Code: check app bundle and 'code' CLI
186
+ installed = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
101
187
  } else if (ide.id === 'antigravity') {
102
- // Antigravity is just a provider here, maybe assume installed if running?
103
- // Or check for some component. For now default to true or false based on something simple.
104
- installed = true; // Assume available
188
+ // Antigravity: check app bundle and 'antigravity' binary
189
+ installed = await checkAppOrBinary(['Antigravity'], ['antigravity']);
105
190
  }
106
191
 
107
192
  if (installed) {
@@ -140,84 +225,117 @@ async function checkFirstRun() {
140
225
  selectedIDEs = response.selectedIDEs;
141
226
  }
142
227
 
143
- // 4. Handle Installations
144
- if (selectedIDEs.includes('kiro')) {
145
- const kiroInstalled = isKiroInstalled();
146
- if (!kiroInstalled) {
147
- // installKiro logs its own inputs, but we want to handle failure gracefully
148
- const success = await installKiro();
149
- if (!success) {
150
- console.log(chalk.yellow('\n⚠️ Kiro installation failed or was skipped.'));
151
- console.log(chalk.gray('You can continue setting up other IDEs.'));
152
- await new Promise(resolve => setTimeout(resolve, 2000));
228
+ // 4. Handle Installations (Generic)
229
+ // For any selected IDE that wasn't detected, try to invoke a matching installer module
230
+ // Installer modules are expected to live next to this file as `<id>-installer.js` and
231
+ // export an installation function like `install<IdPascal>()` or `install`.
232
+ for (const ideId of selectedIDEs) {
233
+ // determine if already installed using existing checks
234
+ let alreadyInstalled = false;
235
+ try {
236
+ if (ideId === 'kiro') {
237
+ alreadyInstalled = isKiroInstalled() || await checkAppOrBinary(['AWS Kiro', 'Kiro'], ['kiro']);
238
+ } else if (ideId === 'cursor') {
239
+ alreadyInstalled = await checkAppOrBinary(['Cursor'], ['cursor']);
240
+ } else if (ideId === 'windsurf') {
241
+ alreadyInstalled = await checkAppOrBinary(['Windsurf'], ['windsurf']);
242
+ } else if (ideId === 'vscode') {
243
+ alreadyInstalled = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
244
+ } else if (ideId === 'antigravity') {
245
+ alreadyInstalled = await checkAppOrBinary(['Antigravity'], ['antigravity']);
153
246
  } else {
154
- // Post-install notification with auto-launch
155
- console.log('\n' + boxen(
156
- chalk.bold.yellow('⚠️ ACTION REQUIRED: SIGN IN ') + '\n\n' +
157
- chalk.white('1. I will launch AWS Kiro IDE for you.') + '\n' +
158
- chalk.white('2. Sign in with Google, GitHub, or AWS Builder ID.') + '\n' +
159
- chalk.white('3. Click "Skip all" to skip survey questions.') + '\n' +
160
- chalk.white('4. Once signed in, you can minimize or close the IDE.') + '\n\n' +
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
- ));
247
+ // default fallback: try binary name same as id
248
+ alreadyInstalled = await checkAppOrBinary([], [ideId]);
249
+ }
250
+ } catch (e) {
251
+ // detection error: assume not installed
252
+ alreadyInstalled = false;
253
+ }
169
254
 
170
- await inquirer.prompt([{
171
- type: 'input',
172
- name: 'launch',
173
- message: 'Press Enter and I will launch Kiro for you to sign in to...'
174
- }]);
255
+ if (alreadyInstalled) continue;
256
+
257
+ // Attempt to locate and run an installer module for this IDE
258
+ let installerModule = null;
259
+ const tryPaths = [`./${ideId}-installer`, `../utils/${ideId}-installer`];
260
+ for (const p of tryPaths) {
261
+ try {
262
+ // require may throw if module doesn't exist
263
+ installerModule = require(p);
264
+ break;
265
+ } catch (e) {
266
+ // ignore and try next
267
+ }
268
+ }
175
269
 
176
- const { exec } = require('child_process');
177
- console.log(chalk.gray('Launching AWS Kiro...'));
270
+ if (!installerModule) {
271
+ console.log(chalk.gray(`No installer module found for ${ideId}, skipping automated install.`));
272
+ continue;
273
+ }
178
274
 
179
- try {
180
- // Try to open AWS Kiro
181
- exec('open -a "AWS Kiro"', (error) => {
182
- if (error) {
183
- // Fallback to just Kiro
184
- exec('open -a "Kiro"', (err) => {
185
- if (err) {
186
- console.log(chalk.red('Could not auto-launch Kiro. Please open it manually.'));
187
- }
188
- });
189
- }
190
- });
191
- } catch (e) {
192
- // Ignore errors, user can launch manually
193
- }
275
+ // Resolve a callable install function from the module
276
+ const pascal = ideId.split(/[-_]/).map(s => s[0].toUpperCase() + s.slice(1)).join('');
277
+ const candidateNames = [`install${pascal}`, 'install', pascal, 'default'];
278
+ let installerFn = null;
279
+ for (const name of candidateNames) {
280
+ if (name === 'default' && typeof installerModule === 'function') {
281
+ installerFn = installerModule;
282
+ break;
283
+ }
284
+ if (installerModule && typeof installerModule[name] === 'function') {
285
+ installerFn = installerModule[name];
286
+ break;
287
+ }
288
+ }
289
+
290
+ if (!installerFn) {
291
+ console.log(chalk.gray(`Installer found for ${ideId} but no callable export (tried ${candidateNames.join(', ')}).`));
292
+ continue;
293
+ }
194
294
 
195
- await inquirer.prompt([{
196
- type: 'input',
197
- name: 'continue',
198
- message: 'Press Enter once you have signed in...'
199
- }]);
295
+ console.log(chalk.cyan(`\n🔧 Installing ${ideId}...`));
296
+ try {
297
+ const ok = await installerFn();
298
+ if (!ok) {
299
+ console.log(chalk.yellow(`\n⚠️ ${ideId} installation failed or was skipped.`));
300
+ } else {
301
+ console.log(chalk.green(`\n✅ ${ideId} installation complete.`));
200
302
  }
303
+ } catch (err) {
304
+ console.log(chalk.red(`${ideId} installation error:`), err.message || err);
201
305
  }
202
306
  }
203
307
 
204
308
  // 5. Configure Preferences
205
- // Enable detected IDEs AND selected IDEs, disable others
309
+ // Re-detect IDEs to ensure we only enable actually installed ones
310
+ const reDetected = [];
311
+ for (const ideDef of ideDefinitions) {
312
+ let installedNow = false;
313
+ if (ideDef.id === 'kiro') {
314
+ installedNow = isKiroInstalled() || await checkAppOrBinary(['AWS Kiro', 'Kiro'], ['kiro']);
315
+ } else if (ideDef.id === 'cursor') {
316
+ installedNow = await checkAppOrBinary(['Cursor'], ['cursor']);
317
+ } else if (ideDef.id === 'windsurf') {
318
+ installedNow = await checkAppOrBinary(['Windsurf'], ['windsurf']);
319
+ } else if (ideDef.id === 'vscode') {
320
+ installedNow = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
321
+ } else if (ideDef.id === 'antigravity') {
322
+ installedNow = await checkAppOrBinary(['Antigravity'], ['antigravity']);
323
+ }
324
+ if (installedNow) reDetected.push(ideDef.id);
325
+ }
326
+
206
327
  const defaultOrder = getDefaultProviderOrder();
207
328
  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
329
+ for (const id of defaultOrder) {
212
330
  const def = definitions.find(d => d.id === id);
213
331
  if (def && def.type === 'ide') {
214
- const isDetected = detectedIDEs.some(d => d.id === id);
332
+ const isDetectedNow = reDetected.includes(id);
215
333
  const isSelected = selectedIDEs.includes(id);
216
- enabledMap[id] = isDetected || isSelected;
334
+ enabledMap[id] = isDetectedNow || isSelected;
217
335
  } else {
218
336
  enabledMap[id] = true; // Keep LLMs enabled by default
219
337
  }
220
- });
338
+ }
221
339
 
222
340
  // Save initial preferences. provider-registry.js usually saves to ~/.config/vibecodingmachine/config.json
223
341
  // But here we are checking ~/.vibecodingmachine.
@@ -261,7 +379,7 @@ async function checkFirstRun() {
261
379
  await inquirer.prompt([{
262
380
  type: 'input',
263
381
  name: 'continue',
264
- message: 'Press Enter to continue...'
382
+ message: t('interactive.press.enter.continue')
265
383
  }]);
266
384
  }
267
385
 
@@ -278,7 +396,7 @@ async function checkFirstRun() {
278
396
  }
279
397
  ));
280
398
 
281
- console.log(chalk.cyan('Press any key to continue to the main menu...'));
399
+ console.log(chalk.cyan(t('interactive.press.any.key.menu')));
282
400
 
283
401
  await new Promise((resolve) => {
284
402
  process.stdin.setRawMode(true);