vibecodingmachine-cli 2025.11.2-9.855 → 2025.12.6-1702

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.
@@ -2,16 +2,98 @@ const path = require('path');
2
2
  const fs = require('fs-extra');
3
3
  const chalk = require('chalk');
4
4
  const chokidar = require('chokidar');
5
- const { getRepoPath } = require('../utils/config');
5
+ const { getRepoPath, setRepoPath } = require('../utils/config');
6
6
  const { getRequirementsPath } = require('vibecodingmachine-core');
7
7
 
8
8
  async function getReqPathOrExit() {
9
- const repoPath = await getRepoPath();
9
+ let repoPath = await getRepoPath();
10
+
11
+ // Auto-detect and initialize if we're in a git repository
12
+ if (!repoPath) {
13
+ const cwd = process.cwd();
14
+ const cwdResolved = path.resolve(cwd);
15
+
16
+ // Find the git root by walking up the directory tree
17
+ let currentDir = cwdResolved;
18
+ let foundGitRoot = false;
19
+ const maxDepth = 50; // Prevent infinite loops
20
+ let depth = 0;
21
+
22
+ // Walk up the directory tree looking for .git directory
23
+ while (depth < maxDepth && currentDir !== path.dirname(currentDir)) {
24
+ const gitPath = path.join(currentDir, '.git');
25
+ try {
26
+ if (await fs.pathExists(gitPath)) {
27
+ repoPath = currentDir;
28
+ foundGitRoot = true;
29
+ break;
30
+ }
31
+ } catch (err) {
32
+ // If we can't check, continue to parent
33
+ }
34
+ const parentDir = path.dirname(currentDir);
35
+ if (parentDir === currentDir) {
36
+ // Reached filesystem root
37
+ break;
38
+ }
39
+ currentDir = parentDir;
40
+ depth++;
41
+ }
42
+
43
+ // Fallback: if no .git found but .vibecodingmachine exists, use current dir
44
+ if (!foundGitRoot) {
45
+ try {
46
+ const vibecodingmachineDir = path.join(cwdResolved, '.vibecodingmachine');
47
+ if (await fs.pathExists(vibecodingmachineDir)) {
48
+ repoPath = cwdResolved;
49
+ foundGitRoot = true;
50
+ }
51
+ } catch (err) {
52
+ // Ignore errors
53
+ }
54
+ }
55
+
56
+ // Final fallback: use current directory if we have package.json
57
+ if (!foundGitRoot) {
58
+ try {
59
+ const packageJsonPath = path.join(cwdResolved, 'package.json');
60
+ if (await fs.pathExists(packageJsonPath)) {
61
+ repoPath = cwdResolved;
62
+ foundGitRoot = true;
63
+ }
64
+ } catch (err) {
65
+ // Ignore errors
66
+ }
67
+ }
68
+
69
+ // If we found a repo path, save it
70
+ if (repoPath) {
71
+ try {
72
+ await setRepoPath(repoPath);
73
+ } catch (err) {
74
+ // If setting fails, we still have repoPath set, so continue
75
+ }
76
+ }
77
+ }
78
+
79
+ // Final validation - ensure we have a valid path
80
+ // If we still don't have a repoPath, use current directory as absolute last resort
81
+ if (!repoPath) {
82
+ repoPath = path.resolve(process.cwd());
83
+ try {
84
+ await setRepoPath(repoPath);
85
+ } catch (err) {
86
+ // Even if we can't save it, we'll use it for this session
87
+ }
88
+ }
89
+
90
+ // At this point, repoPath MUST be set (we've tried everything including current directory)
10
91
  if (!repoPath) {
11
92
  console.log(chalk.yellow('No repository path configured'));
12
93
  console.log(chalk.gray('Use'), chalk.cyan('vcm repo:init'), chalk.gray('or'), chalk.cyan('vcm repo:set <path>'));
13
94
  process.exit(1);
14
95
  }
96
+
15
97
  // Use getRequirementsPath which handles hostname-specific files
16
98
  const reqPath = await getRequirementsPath(repoPath);
17
99
  return { repoPath, reqPath };
@@ -40,7 +122,7 @@ async function list(options) {
40
122
  }
41
123
  }
42
124
 
43
- async function add(name) {
125
+ async function add(name, pkg, description) {
44
126
  try {
45
127
  const { reqPath } = await getReqPathOrExit();
46
128
  await fs.ensureFile(reqPath);
@@ -64,6 +146,34 @@ async function add(name) {
64
146
  if (!inserted && lines[i].includes(todoSectionHeader)) {
65
147
  // Add requirement header
66
148
  newLines.push(`### ${name}`);
149
+
150
+ // Add package if provided (and not 'all')
151
+ if (pkg && Array.isArray(pkg) && pkg.length > 0) {
152
+ const packageValue = pkg.length === 1 ? pkg[0] : pkg.join(', ');
153
+ if (packageValue !== 'all') {
154
+ newLines.push(`PACKAGE: ${packageValue}`);
155
+ }
156
+ } else if (pkg && typeof pkg === 'string' && pkg !== 'all') {
157
+ newLines.push(`PACKAGE: ${pkg}`);
158
+ }
159
+
160
+ // Add description if provided
161
+ if (description && typeof description === 'string' && description.trim()) {
162
+ const descLines = description.split('\n');
163
+ descLines.forEach(line => {
164
+ if (line.trim()) {
165
+ newLines.push(line);
166
+ }
167
+ });
168
+ } else if (description && Array.isArray(description)) {
169
+ // Handle array of description lines
170
+ description.forEach(line => {
171
+ if (line && typeof line === 'string' && line.trim()) {
172
+ newLines.push(line);
173
+ }
174
+ });
175
+ }
176
+
67
177
  // Add blank line after requirement
68
178
  newLines.push('');
69
179
  inserted = true;
@@ -120,15 +230,10 @@ async function next() {
120
230
  async function edit() {
121
231
  try {
122
232
  const { reqPath } = await getReqPathOrExit();
123
- const opener = process.env.EDITOR ? null : 'open';
124
- if (process.env.EDITOR) {
125
- const { spawn } = require('child_process');
126
- const child = spawn(process.env.EDITOR, [reqPath], { stdio: 'inherit' });
127
- child.on('exit', (code) => process.exit(code || 0));
128
- } else {
129
- const { execSync } = require('child_process');
130
- execSync(`${opener} ${JSON.stringify(reqPath)}`);
131
- }
233
+ // Always use system default opener to avoid taking over the terminal
234
+ const { execSync } = require('child_process');
235
+ // Use 'open' for macOS (user's OS), could be expanded for others if needed
236
+ execSync(`open ${JSON.stringify(reqPath)}`);
132
237
  } catch (error) {
133
238
  console.error(chalk.red('Error opening file:'), error.message);
134
239
  process.exit(1);
@@ -148,13 +253,99 @@ async function watch() {
148
253
  }
149
254
  }
150
255
 
256
+ async function rename(oldTitle, newTitle, description) {
257
+ try {
258
+ const { reqPath } = await getReqPathOrExit();
259
+ if (!await fs.pathExists(reqPath)) {
260
+ console.log(chalk.yellow('No REQUIREMENTS.md found.'));
261
+ return;
262
+ }
263
+
264
+ // Read the requirements file
265
+ const content = await fs.readFile(reqPath, 'utf8');
266
+ const lines = content.split('\n');
267
+
268
+ // Find the requirement block
269
+ let requirementStartIndex = -1;
270
+ let requirementEndIndex = -1;
271
+
272
+ for (let i = 0; i < lines.length; i++) {
273
+ const line = lines[i].trim();
274
+ if (line.startsWith('###')) {
275
+ const title = line.replace(/^###\s*/, '').trim();
276
+ if (title === oldTitle) {
277
+ requirementStartIndex = i;
278
+ // Find the end of this requirement (next ### or ## header)
279
+ for (let j = i + 1; j < lines.length; j++) {
280
+ const nextLine = lines[j].trim();
281
+ if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
282
+ requirementEndIndex = j;
283
+ break;
284
+ }
285
+ }
286
+ if (requirementEndIndex === -1) {
287
+ requirementEndIndex = lines.length;
288
+ }
289
+ break;
290
+ }
291
+ }
292
+ }
293
+
294
+ if (requirementStartIndex === -1) {
295
+ console.log(chalk.yellow(`āš ļø Could not find requirement: "${oldTitle}"`));
296
+ return;
297
+ }
298
+
299
+ // Build new requirement block
300
+ const newBlock = [`### ${newTitle}`];
301
+
302
+ // If description provided, use it
303
+ if (description) {
304
+ if (Array.isArray(description)) {
305
+ description.forEach(line => {
306
+ if (line && line.trim()) {
307
+ newBlock.push(line);
308
+ }
309
+ });
310
+ } else if (typeof description === 'string') {
311
+ description.split('\n').forEach(line => {
312
+ if (line.trim()) {
313
+ newBlock.push(line);
314
+ }
315
+ });
316
+ }
317
+ } else {
318
+ // Keep existing details (skip the ### header line)
319
+ for (let i = requirementStartIndex + 1; i < requirementEndIndex; i++) {
320
+ const line = lines[i];
321
+ if (line.trim()) {
322
+ newBlock.push(line);
323
+ }
324
+ }
325
+ }
326
+
327
+ newBlock.push(''); // Blank line after requirement
328
+
329
+ // Replace the old block with the new one
330
+ lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex, ...newBlock);
331
+
332
+ // Save
333
+ await fs.writeFile(reqPath, lines.join('\n'));
334
+ console.log(chalk.green('āœ“ Requirement renamed/updated'));
335
+ } catch (error) {
336
+ console.error(chalk.red('Error renaming requirement:'), error.message);
337
+ process.exit(1);
338
+ }
339
+ }
340
+
151
341
  module.exports = {
152
342
  list,
153
343
  add,
154
344
  current,
155
345
  next,
156
346
  edit,
157
- watch
347
+ watch,
348
+ rename
158
349
  };
159
350
 
160
351
 
@@ -0,0 +1,280 @@
1
+ const chalk = require('chalk');
2
+ const Table = require('cli-table3');
3
+ const SyncEngine = require('vibecodingmachine-core/src/sync/sync-engine');
4
+
5
+ /**
6
+ * Trigger immediate sync
7
+ */
8
+ async function syncNow(options = {}) {
9
+ const syncEngine = new SyncEngine();
10
+
11
+ try {
12
+ console.log(chalk.cyan('\nšŸ”„ Starting sync...\n'));
13
+
14
+ await syncEngine.initialize();
15
+
16
+ // Listen for sync completion
17
+ const syncPromise = new Promise((resolve, reject) => {
18
+ syncEngine.once('sync-complete', (result) => {
19
+ if (result.status === 'error') {
20
+ reject(new Error(result.error || 'Sync failed'));
21
+ } else {
22
+ resolve(result);
23
+ }
24
+ });
25
+ });
26
+
27
+ // Start sync
28
+ await syncEngine.sync();
29
+
30
+ // Wait for completion
31
+ const result = await syncPromise;
32
+
33
+ const status = syncEngine.getStatus();
34
+
35
+ console.log(chalk.green('āœ“ Sync complete!\n'));
36
+ console.log(chalk.white('Last Sync: ') + new Date(status.lastSyncTime).toLocaleString());
37
+ console.log(chalk.white('Remote Changes: ') + (result.remoteChanges || 0));
38
+ console.log(chalk.white('Local Changes: ') + (result.localChanges || 0));
39
+ console.log(chalk.white('Conflicts: ') + (result.conflicts || 0));
40
+ console.log(chalk.white('Queued Changes: ') + status.queuedChanges);
41
+ console.log('');
42
+
43
+ } catch (error) {
44
+ console.error(chalk.red('\nāœ— Sync failed:'), error.message);
45
+ console.log(chalk.gray('\nTip: Check AWS credentials and DynamoDB table configuration.\n'));
46
+ // Don't throw - just log the error
47
+ } finally {
48
+ syncEngine.stop();
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Show sync status and statistics
54
+ */
55
+ async function syncStatus(options = {}) {
56
+ const syncEngine = new SyncEngine();
57
+
58
+ try {
59
+ await syncEngine.initialize();
60
+
61
+ const status = syncEngine.getStatus();
62
+
63
+ console.log('\n' + chalk.bold.cyan('Sync Status'));
64
+ console.log(chalk.gray('─'.repeat(60)));
65
+
66
+ // Connection status
67
+ const onlineIcon = status.isOnline ? chalk.green('ā—') : chalk.red('ā—');
68
+ const onlineText = status.isOnline ? 'Online' : 'Offline';
69
+ console.log(chalk.white('Connection: ') + onlineIcon + ' ' + onlineText);
70
+
71
+ // Sync status
72
+ const syncingIcon = status.isSyncing ? chalk.yellow('⟳') : chalk.gray('ā—‹');
73
+ const syncingText = status.isSyncing ? 'Syncing...' : 'Idle';
74
+ console.log(chalk.white('Status: ') + syncingIcon + ' ' + syncingText);
75
+
76
+ // Last sync
77
+ const lastSync = status.lastSyncTime
78
+ ? new Date(status.lastSyncTime).toLocaleString()
79
+ : 'Never';
80
+ console.log(chalk.white('Last Sync: ') + lastSync);
81
+
82
+ // Queued changes
83
+ const queueColor = status.queuedChanges > 0 ? chalk.yellow : chalk.gray;
84
+ console.log(chalk.white('Queued Changes: ') + queueColor(status.queuedChanges));
85
+
86
+ // Conflict strategy
87
+ console.log(chalk.white('Conflict Mode: ') + status.conflictStrategy);
88
+
89
+ // Computer ID
90
+ console.log(chalk.white('Computer ID: ') + status.computerId);
91
+
92
+ console.log('');
93
+
94
+ // Show recent sync events
95
+ const history = syncEngine.getHistory(5);
96
+ if (history.length > 0) {
97
+ console.log(chalk.bold.cyan('Recent Sync Events'));
98
+ console.log(chalk.gray('─'.repeat(60)));
99
+
100
+ for (const event of history.reverse()) {
101
+ const time = new Date(event.timestamp).toLocaleTimeString();
102
+ const icon = event.type === 'conflict-resolution' ? chalk.yellow('⚠') : chalk.gray('•');
103
+ console.log(`${icon} ${chalk.gray(time)} ${event.type}`);
104
+ }
105
+
106
+ console.log('');
107
+ }
108
+
109
+ } catch (error) {
110
+ console.error(chalk.red('\nāœ— Failed to get sync status:'), error.message);
111
+ throw error;
112
+ } finally {
113
+ syncEngine.stop();
114
+ }
115
+ }
116
+
117
+ /**
118
+ * View pending changes in offline queue
119
+ */
120
+ async function viewQueue(options = {}) {
121
+ const syncEngine = new SyncEngine();
122
+
123
+ try {
124
+ await syncEngine.initialize();
125
+
126
+ const status = syncEngine.getStatus();
127
+
128
+ if (status.queuedChanges === 0) {
129
+ console.log(chalk.gray('\nNo pending changes in queue.\n'));
130
+ return;
131
+ }
132
+
133
+ console.log(chalk.cyan(`\nšŸ“‹ Offline Queue (${status.queuedChanges} changes)\n`));
134
+
135
+ // Create table
136
+ const table = new Table({
137
+ head: [
138
+ chalk.cyan('#'),
139
+ chalk.cyan('Type'),
140
+ chalk.cyan('Requirement'),
141
+ chalk.cyan('Timestamp')
142
+ ],
143
+ colWidths: [5, 15, 40, 20]
144
+ });
145
+
146
+ // Add rows (accessing internal queue - in production, add a public getter)
147
+ syncEngine.offlineQueue.forEach((change, index) => {
148
+ table.push([
149
+ index + 1,
150
+ change.type || 'update',
151
+ truncate(change.requirementName || change.requirementId || '-', 38),
152
+ new Date(change.timestamp).toLocaleString()
153
+ ]);
154
+ });
155
+
156
+ console.log(table.toString() + '\n');
157
+
158
+ console.log(chalk.gray('These changes will be synced when connection is restored.\n'));
159
+ console.log(chalk.white('Commands:'));
160
+ console.log(chalk.gray(' vcm sync:force ') + '- Force sync now');
161
+ console.log(chalk.gray(' vcm sync:now ') + '- Sync when online');
162
+ console.log('');
163
+
164
+ } catch (error) {
165
+ console.error(chalk.red('\nāœ— Failed to view queue:'), error.message);
166
+ throw error;
167
+ } finally {
168
+ syncEngine.stop();
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Force sync even if offline
174
+ */
175
+ async function forceSync(options = {}) {
176
+ const syncEngine = new SyncEngine();
177
+
178
+ try {
179
+ console.log(chalk.yellow('\n⚠ Forcing sync (ignoring offline status)...\n'));
180
+
181
+ await syncEngine.initialize();
182
+
183
+ // Temporarily set online
184
+ const wasOnline = syncEngine.isOnline;
185
+ syncEngine.setOnlineMode(true);
186
+
187
+ try {
188
+ await syncEngine.sync();
189
+ console.log(chalk.green('āœ“ Force sync complete!\n'));
190
+ } finally {
191
+ // Restore original online status
192
+ syncEngine.setOnlineMode(wasOnline);
193
+ }
194
+
195
+ } catch (error) {
196
+ console.error(chalk.red('\nāœ— Force sync failed:'), error.message);
197
+ console.log(chalk.gray('\nThis usually means the server is unreachable.\n'));
198
+ throw error;
199
+ } finally {
200
+ syncEngine.stop();
201
+ }
202
+ }
203
+
204
+ /**
205
+ * View sync history
206
+ */
207
+ async function viewHistory(options = {}) {
208
+ const syncEngine = new SyncEngine();
209
+
210
+ try {
211
+ await syncEngine.initialize();
212
+
213
+ const limit = parseInt(options.limit) || 50;
214
+ const history = syncEngine.getHistory(limit);
215
+
216
+ if (history.length === 0) {
217
+ console.log(chalk.gray('\nNo sync history available.\n'));
218
+ return;
219
+ }
220
+
221
+ console.log(chalk.cyan(`\nšŸ“œ Sync History (last ${history.length} events)\n`));
222
+
223
+ // Create table
224
+ const table = new Table({
225
+ head: [
226
+ chalk.cyan('Time'),
227
+ chalk.cyan('Type'),
228
+ chalk.cyan('Details')
229
+ ],
230
+ colWidths: [20, 20, 50]
231
+ });
232
+
233
+ // Add rows
234
+ for (const event of history.reverse()) {
235
+ const time = new Date(event.timestamp).toLocaleString();
236
+ const type = event.type;
237
+ const details = getEventDetails(event);
238
+
239
+ table.push([time, type, details]);
240
+ }
241
+
242
+ console.log(table.toString() + '\n');
243
+
244
+ } catch (error) {
245
+ console.error(chalk.red('\nāœ— Failed to view history:'), error.message);
246
+ throw error;
247
+ } finally {
248
+ syncEngine.stop();
249
+ }
250
+ }
251
+
252
+ // Helper functions
253
+
254
+ function truncate(str, maxLength) {
255
+ if (str.length <= maxLength) return str;
256
+ return str.substring(0, maxLength - 3) + '...';
257
+ }
258
+
259
+ function getEventDetails(event) {
260
+ switch (event.type) {
261
+ case 'conflict-resolution':
262
+ return `Resolved: ${event.resolution?.requirementId || 'unknown'}`;
263
+ case 'sync-complete':
264
+ return `Remote: ${event.remoteChanges || 0}, Local: ${event.localChanges || 0}`;
265
+ case 'remote-change-applied':
266
+ return `Applied: ${event.change?.requirementId || 'unknown'}`;
267
+ case 'local-change-pushed':
268
+ return `Pushed: ${event.change?.requirementId || 'unknown'}`;
269
+ default:
270
+ return JSON.stringify(event).substring(0, 47);
271
+ }
272
+ }
273
+
274
+ module.exports = {
275
+ syncNow,
276
+ syncStatus,
277
+ viewQueue,
278
+ forceSync,
279
+ viewHistory
280
+ };
@@ -0,0 +1,61 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+
5
+ /**
6
+ * Scans the assets directory and removes 0-byte PNG files
7
+ * This is a mitigation for an issue where empty PNG files are mysteriously created
8
+ */
9
+ async function cleanupBrokenAssets() {
10
+ try {
11
+ // Find assets directory - assuming we are in packages/cli/src/utils
12
+ // and assets is at root of repo, but simpler to look relative to where VCM runs
13
+ // effectively, we look for 'assets' in the current working directory as CWD is usually the repo root or where the CLI is run
14
+ // Better: define path relative to the project root if possible, or CWD if that's how it's structured.
15
+ // Based on previous search, assets/ is at the root of the repo.
16
+ // However, when installed, strict paths matters.
17
+ // But for the user's workspace context, iterating 'assets/' in CWD (repo root) is correct for their dev environment.
18
+ // Let's try to be smart about finding specific 'assets' dir if we can, but CWD is a safe start for the CLI tool which is often run from root.
19
+
20
+ // Actually, looking at previous grep, `assets/` was checked specifically.
21
+ // Let's just check `assets` in process.cwd() as a primary target.
22
+
23
+ const assetsDir = path.join(process.cwd(), 'assets');
24
+
25
+ if (!await fs.pathExists(assetsDir)) {
26
+ return; // No assets dir, nothing to clean
27
+ }
28
+
29
+ const files = await fs.readdir(assetsDir);
30
+ let removedCount = 0;
31
+
32
+ for (const file of files) {
33
+ if (file.toLowerCase().endsWith('.png')) {
34
+ const filePath = path.join(assetsDir, file);
35
+ try {
36
+ const stats = await fs.stat(filePath);
37
+ if (stats.size === 0) {
38
+ await fs.remove(filePath);
39
+ removedCount++;
40
+ }
41
+ } catch (err) {
42
+ // Ignore errors accessing specific files
43
+ }
44
+ }
45
+ }
46
+
47
+ if (removedCount > 0) {
48
+ // Only log if we actually did something, to keep noise down
49
+ // But maybe we want to know? The plan said "verification: verify removed".
50
+ // We can use a debug flag or just log.
51
+ // Let's log to console for now as it's a CLI tool.
52
+ // console.log(chalk.gray(`Cleaned up ${removedCount} broken (0-byte) asset files.`));
53
+ }
54
+
55
+ } catch (error) {
56
+ // Silently fail to avoid disrupting startup
57
+ // console.error('Asset cleanup failed:', error);
58
+ }
59
+ }
60
+
61
+ module.exports = { cleanupBrokenAssets };