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.
- package/bin/vibecodingmachine.js +92 -4
- package/package.json +5 -2
- package/repro_open.js +13 -0
- package/scripts/postinstall.js +80 -0
- package/src/commands/auto-direct.js +401 -97
- package/src/commands/auto.js +343 -115
- package/src/commands/computers.js +306 -0
- package/src/commands/requirements-remote.js +304 -0
- package/src/commands/requirements.js +204 -13
- package/src/commands/sync.js +280 -0
- package/src/utils/asset-cleanup.js +61 -0
- package/src/utils/auth.js +84 -7
- package/src/utils/auto-mode-simple-ui.js +2 -22
- package/src/utils/first-run.js +293 -0
- package/src/utils/interactive.js +1027 -217
- package/src/utils/kiro-installer.js +146 -0
- package/src/utils/provider-registry.js +8 -0
- package/src/utils/status-card.js +2 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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 };
|