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.
@@ -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 };
@@ -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, installKiro } = require('./kiro-installer');
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
- if (platform === 'darwin') installed = await fs.pathExists(getAppPath('Cursor'));
178
+ // Cursor: check app bundle and 'cursor' binary
179
+ installed = await checkAppOrBinary(['Cursor'], ['cursor']);
97
180
  } else if (ide.id === 'windsurf') {
98
- if (platform === 'darwin') installed = await fs.pathExists(getAppPath('Windsurf'));
181
+ // Windsurf: check app bundle and common binary
182
+ installed = await checkAppOrBinary(['Windsurf'], ['windsurf']);
99
183
  } else if (ide.id === 'vscode') {
100
- if (platform === 'darwin') installed = await fs.pathExists(getAppPath('Visual Studio Code'));
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 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
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
- 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));
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
- // 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
- ));
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
- 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
- }]);
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
- const { exec } = require('child_process');
177
- console.log(chalk.gray('Launching AWS Kiro...'));
269
+ if (!installerModule) {
270
+ console.log(chalk.gray(`No installer module found for ${ideId}, skipping automated install.`));
271
+ continue;
272
+ }
178
273
 
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
- }
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
- await inquirer.prompt([{
196
- type: 'input',
197
- name: 'continue',
198
- message: 'Press Enter once you have signed in...'
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
- // Enable detected IDEs AND selected IDEs, disable others
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 isDetected = detectedIDEs.some(d => d.id === id);
331
+ const isDetectedNow = reDetected.includes(id);
215
332
  const isSelected = selectedIDEs.includes(id);
216
- enabledMap[id] = isDetected || isSelected;
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.