vibecodingmachine-cli 2025.12.1-534 → 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.
- package/bin/vibecodingmachine.js +301 -12
- package/package.json +5 -2
- package/repro_open.js +13 -0
- package/reproduce_issue.js +160 -0
- package/scripts/postinstall.js +80 -0
- package/src/commands/auth.js +0 -1
- package/src/commands/auto-direct.js +455 -136
- package/src/commands/auto.js +488 -163
- package/src/commands/computers.js +306 -0
- package/src/commands/repo.js +0 -1
- package/src/commands/requirements-remote.js +308 -0
- package/src/commands/requirements.js +233 -16
- package/src/commands/status.js +0 -1
- package/src/commands/sync.js +280 -0
- package/src/utils/agent-selector.js +50 -0
- package/src/utils/antigravity-installer.js +212 -0
- package/src/utils/antigravity-js-handler.js +60 -0
- package/src/utils/asset-cleanup.js +60 -0
- package/src/utils/auth.js +232 -8
- package/src/utils/auto-mode-ansi-ui.js +0 -1
- package/src/utils/auto-mode-simple-ui.js +3 -23
- package/src/utils/compliance-check.js +166 -0
- package/src/utils/config.js +27 -1
- package/src/utils/copy-with-progress.js +167 -0
- package/src/utils/download-with-progress.js +84 -0
- package/src/utils/first-run.js +410 -0
- package/src/utils/interactive.js +1197 -391
- package/src/utils/kiro-installer.js +178 -0
- package/src/utils/persistent-header.js +1 -3
- package/src/utils/provider-registry.js +13 -4
- package/src/utils/status-card.js +2 -1
- package/src/utils/user-tracking.js +300 -0
- package/tests/requirements-navigator-buildtree-await.test.js +28 -0
|
@@ -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);
|
|
@@ -48,7 +130,7 @@ async function add(name) {
|
|
|
48
130
|
|
|
49
131
|
// Find the TODO section
|
|
50
132
|
const todoSectionHeader = '## ⏳ Requirements not yet completed';
|
|
51
|
-
if (!content.includes(
|
|
133
|
+
if (!content.includes('Requirements not yet completed')) {
|
|
52
134
|
content += '\n\n' + todoSectionHeader + '\n';
|
|
53
135
|
}
|
|
54
136
|
|
|
@@ -60,10 +142,38 @@ async function add(name) {
|
|
|
60
142
|
for (let i = 0; i < lines.length; i++) {
|
|
61
143
|
newLines.push(lines[i]);
|
|
62
144
|
|
|
63
|
-
// Insert right after the TODO section header
|
|
64
|
-
if (!inserted && lines[i].includes(
|
|
145
|
+
// Insert right after the TODO section header
|
|
146
|
+
if (!inserted && lines[i].startsWith('##') && lines[i].includes('Requirements not yet completed')) {
|
|
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,125 @@ 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
|
+
|
|
341
|
+
async function working() {
|
|
342
|
+
try {
|
|
343
|
+
const { reqPath } = await getReqPathOrExit();
|
|
344
|
+
if (!await fs.pathExists(reqPath)) {
|
|
345
|
+
console.log(chalk.yellow('No REQUIREMENTS.md found.'));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const { getCurrentRequirement } = require('vibecodingmachine-core');
|
|
349
|
+
// Note: Core's getCurrentRequirement returns "Working on: X" or default text.
|
|
350
|
+
// It assumes process.cwd() or similar.
|
|
351
|
+
// But wait, Core's getCurrentRequirement takes 'repoPath'.
|
|
352
|
+
// And imported from core index.cjs might need repoPath.
|
|
353
|
+
|
|
354
|
+
// Let's manually implement it to be safe and use getReqPathOrExit's resolved path
|
|
355
|
+
// OR use the core helper but pass the correct repo path.
|
|
356
|
+
const { repoPath } = await getReqPathOrExit();
|
|
357
|
+
const coreResult = await getCurrentRequirement(repoPath);
|
|
358
|
+
console.log(chalk.cyan(coreResult));
|
|
359
|
+
|
|
360
|
+
} catch (error) {
|
|
361
|
+
console.error(chalk.red('Error getting working requirement:'), error.message);
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
151
366
|
module.exports = {
|
|
152
367
|
list,
|
|
153
368
|
add,
|
|
154
369
|
current,
|
|
370
|
+
working,
|
|
155
371
|
next,
|
|
156
372
|
edit,
|
|
157
|
-
watch
|
|
373
|
+
watch,
|
|
374
|
+
rename
|
|
158
375
|
};
|
|
159
376
|
|
|
160
377
|
|
package/src/commands/status.js
CHANGED
|
@@ -64,7 +64,6 @@ async function progress() {
|
|
|
64
64
|
|
|
65
65
|
async function logs(cmd) {
|
|
66
66
|
const lines = parseInt((cmd && cmd.lines) || '50', 10);
|
|
67
|
-
const repoPath = await getRepoPath();
|
|
68
67
|
const defaultLog = path.join(process.cwd(), 'logs', 'security', 'security-2025-10-29.log');
|
|
69
68
|
const logPath = await fs.pathExists(defaultLog) ? defaultLog : null;
|
|
70
69
|
console.log(chalk.bold('\nRecent Logs'));
|
|
@@ -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() {
|
|
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() {
|
|
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() {
|
|
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() {
|
|
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,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized agent selection logic
|
|
3
|
+
* Respects provider preferences order and ensures DRY principle
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { getProviderPreferences } = require('./provider-registry');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get the effective agent based on provider preferences and options
|
|
10
|
+
* @param {Object} options - Command line options (may include ide)
|
|
11
|
+
* @param {Array} providerDefinitions - Array of provider definitions
|
|
12
|
+
* @param {Map} providerDefinitionMap - Map of provider definitions by ID
|
|
13
|
+
* @returns {Object} - { effectiveAgent: string, providerDef: Object }
|
|
14
|
+
*/
|
|
15
|
+
async function getEffectiveAgent(options = {}, providerDefinitions, providerDefinitionMap) {
|
|
16
|
+
const prefs = await getProviderPreferences();
|
|
17
|
+
|
|
18
|
+
// Get all available providers in the order specified in preferences
|
|
19
|
+
const availableProviders = [];
|
|
20
|
+
for (const id of prefs.order) {
|
|
21
|
+
if (prefs.enabled[id] !== false && providerDefinitionMap.has(id)) {
|
|
22
|
+
availableProviders.push(id);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// If no providers are available, use the first one from definitions as fallback
|
|
27
|
+
if (availableProviders.length === 0) {
|
|
28
|
+
availableProviders.push(providerDefinitions[0]?.id || 'claude-code');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Use the first available provider by default, unless overridden by options
|
|
32
|
+
let effectiveAgent = options.ide || availableProviders[0];
|
|
33
|
+
|
|
34
|
+
// If the requested agent isn't available, use the first available one
|
|
35
|
+
if (!availableProviders.includes(effectiveAgent)) {
|
|
36
|
+
effectiveAgent = availableProviders[0];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const providerDef = providerDefinitionMap.get(effectiveAgent);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
effectiveAgent,
|
|
43
|
+
providerDef,
|
|
44
|
+
availableProviders
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
getEffectiveAgent
|
|
50
|
+
};
|