vigthoria-cli 1.6.53 → 1.6.55
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/dist/commands/chat.js +30 -0
- package/dist/commands/preview.d.ts +55 -0
- package/dist/commands/preview.js +478 -0
- package/dist/index.js +24 -0
- package/dist/utils/api.d.ts +1 -0
- package/dist/utils/api.js +17 -0
- package/dist/utils/workspace-stream.d.ts +75 -0
- package/dist/utils/workspace-stream.js +301 -0
- package/package.json +3 -2
package/dist/commands/chat.js
CHANGED
|
@@ -47,6 +47,7 @@ const api_js_1 = require("../utils/api.js");
|
|
|
47
47
|
const tools_js_1 = require("../utils/tools.js");
|
|
48
48
|
const session_js_1 = require("../utils/session.js");
|
|
49
49
|
const bridge_client_js_1 = require("../utils/bridge-client.js");
|
|
50
|
+
const workspace_stream_js_1 = require("../utils/workspace-stream.js");
|
|
50
51
|
const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
|
|
51
52
|
const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS || '3900000';
|
|
52
53
|
const parsed = Number.parseInt(rawValue, 10);
|
|
@@ -1442,6 +1443,17 @@ class ChatCommand {
|
|
|
1442
1443
|
targetPath: this.currentProjectPath,
|
|
1443
1444
|
...runtimeContext,
|
|
1444
1445
|
};
|
|
1446
|
+
// Start workspace watcher for bidirectional real-time sync
|
|
1447
|
+
let watcher = null;
|
|
1448
|
+
if (this.currentProjectPath && fs.existsSync(this.currentProjectPath)) {
|
|
1449
|
+
watcher = new workspace_stream_js_1.WorkspaceWatcher({
|
|
1450
|
+
workspaceRoot: this.currentProjectPath,
|
|
1451
|
+
onFileChange: (relativePath, content, action) => {
|
|
1452
|
+
this.logger.debug(`Local change detected: ${action} ${relativePath}`);
|
|
1453
|
+
},
|
|
1454
|
+
});
|
|
1455
|
+
watcher.start();
|
|
1456
|
+
}
|
|
1445
1457
|
try {
|
|
1446
1458
|
const workflowPromise = this.api.runV3AgentWorkflow(executionPrompt, {
|
|
1447
1459
|
workspace: { path: this.currentProjectPath },
|
|
@@ -1484,6 +1496,7 @@ class ChatCommand {
|
|
|
1484
1496
|
}
|
|
1485
1497
|
this.logger.warn('Falling back to legacy CLI agent loop');
|
|
1486
1498
|
this.logger.debug(`V3 agent workflow returned an incomplete result: ${previewGate?.error || 'workspace changes were not fully validated'}`);
|
|
1499
|
+
watcher?.stop();
|
|
1487
1500
|
return false;
|
|
1488
1501
|
}
|
|
1489
1502
|
const errorMessage = `V3 agent workflow returned an incomplete result and legacy fallback is disabled. ${previewGate?.error || 'Workspace changes were not fully validated.'}`;
|
|
@@ -1504,6 +1517,7 @@ class ChatCommand {
|
|
|
1504
1517
|
metadata: { executionPath: 'v3-agent', previewGate },
|
|
1505
1518
|
}, null, 2));
|
|
1506
1519
|
}
|
|
1520
|
+
watcher?.stop();
|
|
1507
1521
|
return true;
|
|
1508
1522
|
}
|
|
1509
1523
|
if (!this.jsonOutput && previewGate?.required && previewGate?.passed !== true && workspaceHasOutput) {
|
|
@@ -1542,10 +1556,26 @@ class ChatCommand {
|
|
|
1542
1556
|
console.log(chalk_1.default.yellow(`Template Service preview gate: failed${previewGate.error ? ` - ${previewGate.error}` : ''}`));
|
|
1543
1557
|
}
|
|
1544
1558
|
}
|
|
1559
|
+
// Show change summary for files touched by the agent
|
|
1560
|
+
if (!this.jsonOutput && !this.directPromptMode && response.changedFiles) {
|
|
1561
|
+
const fileCount = Object.keys(response.changedFiles).length;
|
|
1562
|
+
if (fileCount > 0) {
|
|
1563
|
+
console.log(chalk_1.default.gray(`\nFiles changed: ${fileCount}`));
|
|
1564
|
+
for (const relPath of Object.keys(response.changedFiles).slice(0, 15)) {
|
|
1565
|
+
console.log(chalk_1.default.gray(` ${chalk_1.default.green('+')} ${relPath}`));
|
|
1566
|
+
}
|
|
1567
|
+
if (fileCount > 15) {
|
|
1568
|
+
console.log(chalk_1.default.gray(` ... and ${fileCount - 15} more`));
|
|
1569
|
+
}
|
|
1570
|
+
console.log(chalk_1.default.gray(`Run ${chalk_1.default.cyan('vigthoria preview --diff')} for full visual diffs.`));
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1545
1573
|
this.messages.push({ role: 'assistant', content: response.content || 'V3 agent workflow completed.' });
|
|
1574
|
+
watcher?.stop();
|
|
1546
1575
|
return true;
|
|
1547
1576
|
}
|
|
1548
1577
|
catch (error) {
|
|
1578
|
+
watcher?.stop();
|
|
1549
1579
|
if (rescueEligible && !this.api.hasAgentWorkspaceOutput(workspaceContext)) {
|
|
1550
1580
|
const rescued = await this.tryCommandLevelSaaSRescue(executionPrompt, prompt, workspaceContext, routingPolicy, spinner, error);
|
|
1551
1581
|
if (rescued) {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vigthoria CLI - Preview Command
|
|
3
|
+
*
|
|
4
|
+
* Local preview server + consolidated visual diffs + Template Service proof gate
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* vigthoria preview - Preview project in browser
|
|
8
|
+
* vigthoria preview --diff - Show consolidated diff of recent agent changes
|
|
9
|
+
* vigthoria preview --proof - Run Template Service preview gate
|
|
10
|
+
* vigthoria preview -p /path/to/project - Preview specific project
|
|
11
|
+
*/
|
|
12
|
+
import { Config } from '../utils/config.js';
|
|
13
|
+
import { Logger } from '../utils/logger.js';
|
|
14
|
+
interface PreviewOptions {
|
|
15
|
+
project: string;
|
|
16
|
+
entry?: string;
|
|
17
|
+
port?: number;
|
|
18
|
+
open?: boolean;
|
|
19
|
+
diff?: boolean;
|
|
20
|
+
proof?: boolean;
|
|
21
|
+
screenshot?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare class PreviewCommand {
|
|
24
|
+
private config;
|
|
25
|
+
private logger;
|
|
26
|
+
private api;
|
|
27
|
+
private server;
|
|
28
|
+
constructor(config: Config, logger: Logger);
|
|
29
|
+
run(options: PreviewOptions): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Detect the HTML entry file in the project
|
|
32
|
+
*/
|
|
33
|
+
private detectEntryFile;
|
|
34
|
+
/**
|
|
35
|
+
* Find an available port starting from the given number
|
|
36
|
+
*/
|
|
37
|
+
private findAvailablePort;
|
|
38
|
+
/**
|
|
39
|
+
* Start a local HTTP server for preview
|
|
40
|
+
*/
|
|
41
|
+
private startServer;
|
|
42
|
+
/**
|
|
43
|
+
* Open URL in default browser
|
|
44
|
+
*/
|
|
45
|
+
private openBrowser;
|
|
46
|
+
/**
|
|
47
|
+
* Show consolidated diff of recent agent changes using git
|
|
48
|
+
*/
|
|
49
|
+
showConsolidatedDiff(projectPath: string): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Run Template Service preview gate and persist proof bundle
|
|
52
|
+
*/
|
|
53
|
+
private runProofGate;
|
|
54
|
+
}
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Vigthoria CLI - Preview Command
|
|
4
|
+
*
|
|
5
|
+
* Local preview server + consolidated visual diffs + Template Service proof gate
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* vigthoria preview - Preview project in browser
|
|
9
|
+
* vigthoria preview --diff - Show consolidated diff of recent agent changes
|
|
10
|
+
* vigthoria preview --proof - Run Template Service preview gate
|
|
11
|
+
* vigthoria preview -p /path/to/project - Preview specific project
|
|
12
|
+
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
47
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
48
|
+
};
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.PreviewCommand = void 0;
|
|
51
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
52
|
+
const fs = __importStar(require("fs"));
|
|
53
|
+
const path = __importStar(require("path"));
|
|
54
|
+
const http = __importStar(require("http"));
|
|
55
|
+
const diff_1 = require("diff");
|
|
56
|
+
const logger_js_1 = require("../utils/logger.js");
|
|
57
|
+
const api_js_1 = require("../utils/api.js");
|
|
58
|
+
// Common MIME types for static file serving
|
|
59
|
+
const MIME_TYPES = {
|
|
60
|
+
'.html': 'text/html',
|
|
61
|
+
'.htm': 'text/html',
|
|
62
|
+
'.css': 'text/css',
|
|
63
|
+
'.js': 'application/javascript',
|
|
64
|
+
'.mjs': 'application/javascript',
|
|
65
|
+
'.json': 'application/json',
|
|
66
|
+
'.png': 'image/png',
|
|
67
|
+
'.jpg': 'image/jpeg',
|
|
68
|
+
'.jpeg': 'image/jpeg',
|
|
69
|
+
'.gif': 'image/gif',
|
|
70
|
+
'.svg': 'image/svg+xml',
|
|
71
|
+
'.ico': 'image/x-icon',
|
|
72
|
+
'.webp': 'image/webp',
|
|
73
|
+
'.woff': 'font/woff',
|
|
74
|
+
'.woff2': 'font/woff2',
|
|
75
|
+
'.ttf': 'font/ttf',
|
|
76
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
77
|
+
'.mp4': 'video/mp4',
|
|
78
|
+
'.webm': 'video/webm',
|
|
79
|
+
'.mp3': 'audio/mpeg',
|
|
80
|
+
'.wav': 'audio/wav',
|
|
81
|
+
'.pdf': 'application/pdf',
|
|
82
|
+
'.xml': 'application/xml',
|
|
83
|
+
'.txt': 'text/plain',
|
|
84
|
+
'.map': 'application/json',
|
|
85
|
+
};
|
|
86
|
+
class PreviewCommand {
|
|
87
|
+
config;
|
|
88
|
+
logger;
|
|
89
|
+
api;
|
|
90
|
+
server = null;
|
|
91
|
+
constructor(config, logger) {
|
|
92
|
+
this.config = config;
|
|
93
|
+
this.logger = logger;
|
|
94
|
+
this.api = new api_js_1.APIClient(config, logger);
|
|
95
|
+
}
|
|
96
|
+
async run(options) {
|
|
97
|
+
const projectPath = path.resolve(options.project || process.cwd());
|
|
98
|
+
if (!fs.existsSync(projectPath) || !fs.statSync(projectPath).isDirectory()) {
|
|
99
|
+
this.logger.error(`Project directory not found: ${projectPath}`);
|
|
100
|
+
this.api.destroy();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
console.log();
|
|
104
|
+
console.log(chalk_1.default.bold.white(` ${logger_js_1.CH.hLine.repeat(3)} Vigthoria Preview ${logger_js_1.CH.hLine.repeat(40)}`));
|
|
105
|
+
console.log(chalk_1.default.gray(` Project: ${projectPath}`));
|
|
106
|
+
console.log();
|
|
107
|
+
// Show consolidated diff of recent agent changes
|
|
108
|
+
if (options.diff) {
|
|
109
|
+
await this.showConsolidatedDiff(projectPath);
|
|
110
|
+
}
|
|
111
|
+
// Run Template Service preview proof gate
|
|
112
|
+
if (options.proof) {
|
|
113
|
+
await this.runProofGate(projectPath, options.screenshot);
|
|
114
|
+
}
|
|
115
|
+
// If only --diff or --proof without a preview server, exit
|
|
116
|
+
if ((options.diff || options.proof) && options.open === false) {
|
|
117
|
+
this.api.destroy();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// Detect entry file
|
|
121
|
+
const entryFile = this.detectEntryFile(projectPath, options.entry);
|
|
122
|
+
if (!entryFile) {
|
|
123
|
+
this.logger.warn('No HTML entry file found. Use --entry <file> to specify one.');
|
|
124
|
+
if (!options.diff && !options.proof) {
|
|
125
|
+
this.api.destroy();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.api.destroy();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Start local preview server
|
|
132
|
+
const port = options.port || (await this.findAvailablePort(3500));
|
|
133
|
+
await this.startServer(projectPath, entryFile, port, options.open !== false);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Detect the HTML entry file in the project
|
|
137
|
+
*/
|
|
138
|
+
detectEntryFile(projectPath, specified) {
|
|
139
|
+
if (specified) {
|
|
140
|
+
const fullPath = path.join(projectPath, specified);
|
|
141
|
+
if (fs.existsSync(fullPath))
|
|
142
|
+
return specified;
|
|
143
|
+
this.logger.warn(`Specified entry file not found: ${specified}`);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
// Common entry point candidates
|
|
147
|
+
const candidates = [
|
|
148
|
+
'index.html',
|
|
149
|
+
'public/index.html',
|
|
150
|
+
'dist/index.html',
|
|
151
|
+
'build/index.html',
|
|
152
|
+
'src/index.html',
|
|
153
|
+
'out/index.html',
|
|
154
|
+
];
|
|
155
|
+
for (const candidate of candidates) {
|
|
156
|
+
if (fs.existsSync(path.join(projectPath, candidate))) {
|
|
157
|
+
return candidate;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Fallback: find any .html file at root
|
|
161
|
+
try {
|
|
162
|
+
const rootFiles = fs.readdirSync(projectPath);
|
|
163
|
+
const htmlFile = rootFiles.find(f => f.endsWith('.html'));
|
|
164
|
+
if (htmlFile)
|
|
165
|
+
return htmlFile;
|
|
166
|
+
}
|
|
167
|
+
catch { /* ignore */ }
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Find an available port starting from the given number
|
|
172
|
+
*/
|
|
173
|
+
findAvailablePort(startPort) {
|
|
174
|
+
return new Promise((resolve) => {
|
|
175
|
+
const testServer = http.createServer();
|
|
176
|
+
testServer.on('error', () => {
|
|
177
|
+
resolve(this.findAvailablePort(startPort + 1));
|
|
178
|
+
});
|
|
179
|
+
testServer.listen(startPort, () => {
|
|
180
|
+
testServer.close(() => resolve(startPort));
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Start a local HTTP server for preview
|
|
186
|
+
*/
|
|
187
|
+
async startServer(projectPath, entryFile, port, autoOpen) {
|
|
188
|
+
return new Promise((resolve) => {
|
|
189
|
+
this.server = http.createServer((req, res) => {
|
|
190
|
+
// Sanitize URL to prevent path traversal
|
|
191
|
+
const urlPath = decodeURIComponent(new URL(req.url || '/', `http://localhost:${port}`).pathname);
|
|
192
|
+
const safePath = path.normalize(urlPath).replace(/^(\.\.[/\\])+/, '');
|
|
193
|
+
let filePath = path.join(projectPath, safePath);
|
|
194
|
+
// Ensure the resolved path is within the project directory
|
|
195
|
+
const resolvedPath = path.resolve(filePath);
|
|
196
|
+
if (!resolvedPath.startsWith(path.resolve(projectPath))) {
|
|
197
|
+
res.writeHead(403);
|
|
198
|
+
res.end('Forbidden');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
// Directory → serve index.html
|
|
202
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
|
|
203
|
+
filePath = path.join(filePath, 'index.html');
|
|
204
|
+
}
|
|
205
|
+
// SPA fallback
|
|
206
|
+
if (!fs.existsSync(filePath)) {
|
|
207
|
+
const spaFallback = path.join(projectPath, entryFile);
|
|
208
|
+
if (fs.existsSync(spaFallback)) {
|
|
209
|
+
filePath = spaFallback;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
res.writeHead(404);
|
|
213
|
+
res.end('Not found');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
218
|
+
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
219
|
+
try {
|
|
220
|
+
const content = fs.readFileSync(filePath);
|
|
221
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
222
|
+
res.end(content);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
res.writeHead(500);
|
|
226
|
+
res.end('Internal server error');
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
this.server.listen(port, () => {
|
|
230
|
+
const url = `http://localhost:${port}/${entryFile}`;
|
|
231
|
+
console.log(chalk_1.default.green(` ${logger_js_1.CH.success} Preview server running`));
|
|
232
|
+
console.log(chalk_1.default.gray(` URL: `) + chalk_1.default.cyan.underline(url));
|
|
233
|
+
console.log(chalk_1.default.gray(` Entry: ${entryFile}`));
|
|
234
|
+
console.log(chalk_1.default.gray(` Press Ctrl+C to stop`));
|
|
235
|
+
console.log();
|
|
236
|
+
if (autoOpen) {
|
|
237
|
+
this.openBrowser(url);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
// Handle graceful shutdown
|
|
241
|
+
const shutdown = () => {
|
|
242
|
+
console.log(chalk_1.default.gray('\n Stopping preview server...'));
|
|
243
|
+
this.server?.close();
|
|
244
|
+
this.api.destroy();
|
|
245
|
+
resolve();
|
|
246
|
+
};
|
|
247
|
+
process.on('SIGINT', shutdown);
|
|
248
|
+
process.on('SIGTERM', shutdown);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Open URL in default browser
|
|
253
|
+
*/
|
|
254
|
+
openBrowser(url) {
|
|
255
|
+
const { exec } = require('child_process');
|
|
256
|
+
const platform = process.platform;
|
|
257
|
+
const cmd = platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open';
|
|
258
|
+
exec(`${cmd} ${url}`, (err) => {
|
|
259
|
+
if (err) {
|
|
260
|
+
this.logger.debug(`Could not auto-open browser: ${err.message}`);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Show consolidated diff of recent agent changes using git
|
|
266
|
+
*/
|
|
267
|
+
async showConsolidatedDiff(projectPath) {
|
|
268
|
+
console.log(chalk_1.default.bold.white(` ${logger_js_1.CH.hLine.repeat(3)} Change Summary ${logger_js_1.CH.hLine.repeat(43)}`));
|
|
269
|
+
console.log();
|
|
270
|
+
const proofDir = path.join(projectPath, '.vigthoria', 'proof', 'preview');
|
|
271
|
+
const hasProof = fs.existsSync(proofDir);
|
|
272
|
+
// Try git diff first (most reliable)
|
|
273
|
+
try {
|
|
274
|
+
const { execSync } = require('child_process');
|
|
275
|
+
const isGit = fs.existsSync(path.join(projectPath, '.git'));
|
|
276
|
+
if (isGit) {
|
|
277
|
+
// Get list of changed files (unstaged + staged)
|
|
278
|
+
const statusOutput = execSync('git status --porcelain', { cwd: projectPath, encoding: 'utf-8' }).trim();
|
|
279
|
+
if (!statusOutput) {
|
|
280
|
+
console.log(chalk_1.default.gray(' No changes detected (working tree clean).'));
|
|
281
|
+
console.log();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const changedLines = statusOutput.split('\n').filter(Boolean);
|
|
285
|
+
const created = [];
|
|
286
|
+
const modified = [];
|
|
287
|
+
const deleted = [];
|
|
288
|
+
for (const line of changedLines) {
|
|
289
|
+
const status = line.substring(0, 2).trim();
|
|
290
|
+
const filePath = line.substring(3);
|
|
291
|
+
if (status === '??' || status === 'A')
|
|
292
|
+
created.push(filePath);
|
|
293
|
+
else if (status === 'D')
|
|
294
|
+
deleted.push(filePath);
|
|
295
|
+
else
|
|
296
|
+
modified.push(filePath);
|
|
297
|
+
}
|
|
298
|
+
// Summary header
|
|
299
|
+
const total = created.length + modified.length + deleted.length;
|
|
300
|
+
console.log(chalk_1.default.white(` ${total} file${total !== 1 ? 's' : ''} changed:`));
|
|
301
|
+
for (const f of created)
|
|
302
|
+
console.log(chalk_1.default.green(` + ${f}`));
|
|
303
|
+
for (const f of modified)
|
|
304
|
+
console.log(chalk_1.default.yellow(` ~ ${f}`));
|
|
305
|
+
for (const f of deleted)
|
|
306
|
+
console.log(chalk_1.default.red(` - ${f}`));
|
|
307
|
+
console.log();
|
|
308
|
+
// Show unified diffs for modified and created files (limit to keep terminal manageable)
|
|
309
|
+
const diffTargets = [...modified, ...created].slice(0, 20);
|
|
310
|
+
for (const relPath of diffTargets) {
|
|
311
|
+
const absPath = path.join(projectPath, relPath);
|
|
312
|
+
if (!fs.existsSync(absPath))
|
|
313
|
+
continue;
|
|
314
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
315
|
+
const textExts = ['.html', '.css', '.js', '.ts', '.jsx', '.tsx', '.json', '.md', '.yml', '.yaml', '.txt', '.xml', '.svg', '.py', '.sh', '.env'];
|
|
316
|
+
if (!textExts.includes(ext))
|
|
317
|
+
continue;
|
|
318
|
+
try {
|
|
319
|
+
const newContent = fs.readFileSync(absPath, 'utf-8');
|
|
320
|
+
let oldContent = '';
|
|
321
|
+
if (modified.includes(relPath)) {
|
|
322
|
+
try {
|
|
323
|
+
oldContent = execSync(`git show HEAD:${relPath}`, { cwd: projectPath, encoding: 'utf-8' });
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// File is new or not tracked
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const patch = (0, diff_1.structuredPatch)(relPath, relPath, oldContent, newContent, 'before', 'after', { context: 3 });
|
|
330
|
+
if (patch.hunks.length === 0)
|
|
331
|
+
continue;
|
|
332
|
+
console.log(chalk_1.default.bold.white(` ${logger_js_1.CH.hLine.repeat(3)} ${relPath} ${logger_js_1.CH.hLine.repeat(Math.max(1, 50 - relPath.length))}`));
|
|
333
|
+
console.log(chalk_1.default.gray(` --- a/${relPath}`));
|
|
334
|
+
console.log(chalk_1.default.gray(` +++ b/${relPath}`));
|
|
335
|
+
for (const hunk of patch.hunks) {
|
|
336
|
+
console.log(chalk_1.default.cyan(` @@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`));
|
|
337
|
+
for (const line of hunk.lines) {
|
|
338
|
+
if (line.startsWith('+')) {
|
|
339
|
+
console.log(chalk_1.default.green(` ${line}`));
|
|
340
|
+
}
|
|
341
|
+
else if (line.startsWith('-')) {
|
|
342
|
+
console.log(chalk_1.default.red(` ${line}`));
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
console.log(chalk_1.default.gray(` ${line}`));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
console.log();
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
// Skip files that can't be diffed
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (diffTargets.length < modified.length + created.length) {
|
|
356
|
+
console.log(chalk_1.default.gray(` ... and ${modified.length + created.length - diffTargets.length} more files`));
|
|
357
|
+
console.log();
|
|
358
|
+
}
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
// git not available, fall through
|
|
364
|
+
}
|
|
365
|
+
// Fallback: list .vigthoria/proof manifests
|
|
366
|
+
if (hasProof) {
|
|
367
|
+
const manifests = fs.readdirSync(proofDir)
|
|
368
|
+
.filter(f => f.endsWith('.json'))
|
|
369
|
+
.sort()
|
|
370
|
+
.reverse()
|
|
371
|
+
.slice(0, 5);
|
|
372
|
+
if (manifests.length > 0) {
|
|
373
|
+
console.log(chalk_1.default.white(' Recent proof bundles:'));
|
|
374
|
+
for (const m of manifests) {
|
|
375
|
+
try {
|
|
376
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(proofDir, m), 'utf-8'));
|
|
377
|
+
const passed = manifest.previewGate?.passed ? chalk_1.default.green('passed') : chalk_1.default.red('failed');
|
|
378
|
+
console.log(chalk_1.default.gray(` ${manifest.createdAt || m} `) + passed + chalk_1.default.gray(` ${manifest.entryPath || '-'}`));
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
console.log(chalk_1.default.gray(` ${m}`));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
console.log();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
console.log(chalk_1.default.gray(' No git history or proof bundles found.'));
|
|
389
|
+
console.log(chalk_1.default.gray(' Run an agent task first to generate changes.'));
|
|
390
|
+
console.log();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Run Template Service preview gate and persist proof bundle
|
|
395
|
+
*/
|
|
396
|
+
async runProofGate(projectPath, captureScreenshot) {
|
|
397
|
+
const spinner = (0, logger_js_1.createSpinner)('Running preview proof gate...').start();
|
|
398
|
+
try {
|
|
399
|
+
const result = await this.api.runTemplateServicePreviewGate('', {
|
|
400
|
+
workspacePath: projectPath,
|
|
401
|
+
projectPath: projectPath,
|
|
402
|
+
targetPath: projectPath,
|
|
403
|
+
});
|
|
404
|
+
spinner.stop();
|
|
405
|
+
console.log(chalk_1.default.bold.white(` ${logger_js_1.CH.hLine.repeat(3)} Preview Proof Gate ${logger_js_1.CH.hLine.repeat(39)}`));
|
|
406
|
+
console.log();
|
|
407
|
+
if (!result.required) {
|
|
408
|
+
console.log(chalk_1.default.gray(' Preview gate: not required (no frontend artifacts detected)'));
|
|
409
|
+
console.log();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const statusIcon = result.passed ? chalk_1.default.green(logger_js_1.CH.success) : chalk_1.default.red(logger_js_1.CH.error);
|
|
413
|
+
console.log(` ${statusIcon} Preview gate: ${result.passed ? chalk_1.default.green('PASSED') : chalk_1.default.red('FAILED')}`);
|
|
414
|
+
if (result.error) {
|
|
415
|
+
console.log(chalk_1.default.yellow(` Error: ${result.error}`));
|
|
416
|
+
}
|
|
417
|
+
// Show modes
|
|
418
|
+
if (result.modes) {
|
|
419
|
+
const modes = result.modes;
|
|
420
|
+
console.log();
|
|
421
|
+
if (modes.design) {
|
|
422
|
+
const designStatus = modes.design.ready ? chalk_1.default.green('ready') : chalk_1.default.gray('not ready');
|
|
423
|
+
console.log(chalk_1.default.gray(` Design mode: `) + designStatus);
|
|
424
|
+
if (modes.design.devices) {
|
|
425
|
+
console.log(chalk_1.default.gray(` Devices: ${modes.design.devices.join(', ')}`));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (modes.live) {
|
|
429
|
+
const liveStatus = modes.live.ready ? chalk_1.default.green('ready') : chalk_1.default.gray('not ready');
|
|
430
|
+
console.log(chalk_1.default.gray(` Live mode: `) + liveStatus);
|
|
431
|
+
if (modes.live.entryPoint) {
|
|
432
|
+
console.log(chalk_1.default.gray(` Entry: ${modes.live.entryPoint}`));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (modes.production) {
|
|
436
|
+
const prodStatus = modes.production.ready ? chalk_1.default.green('ready') : chalk_1.default.gray('not ready');
|
|
437
|
+
console.log(chalk_1.default.gray(` Production mode: `) + prodStatus);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// Show summary
|
|
441
|
+
if (result.summary) {
|
|
442
|
+
console.log();
|
|
443
|
+
const summary = result.summary;
|
|
444
|
+
if (summary.hasViewportMeta !== undefined) {
|
|
445
|
+
console.log(chalk_1.default.gray(` Viewport meta: `) + (summary.hasViewportMeta ? chalk_1.default.green('yes') : chalk_1.default.yellow('missing')));
|
|
446
|
+
}
|
|
447
|
+
if (summary.hasResponsiveSignals !== undefined) {
|
|
448
|
+
console.log(chalk_1.default.gray(` Responsive CSS: `) + (summary.hasResponsiveSignals ? chalk_1.default.green('yes') : chalk_1.default.gray('no')));
|
|
449
|
+
}
|
|
450
|
+
if (summary.hasInteractiveSignals !== undefined) {
|
|
451
|
+
console.log(chalk_1.default.gray(` Interactive JS: `) + (summary.hasInteractiveSignals ? chalk_1.default.green('yes') : chalk_1.default.gray('no')));
|
|
452
|
+
}
|
|
453
|
+
if (typeof summary.sectionCount === 'number') {
|
|
454
|
+
console.log(chalk_1.default.gray(` Section count: ${summary.sectionCount}`));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// Show artifacts
|
|
458
|
+
if (result.artifacts) {
|
|
459
|
+
console.log();
|
|
460
|
+
if (result.artifacts.manifestPath) {
|
|
461
|
+
console.log(chalk_1.default.gray(` Manifest: ${result.artifacts.manifestPath}`));
|
|
462
|
+
}
|
|
463
|
+
if (result.artifacts.screenshotCaptured && result.artifacts.screenshotPath) {
|
|
464
|
+
console.log(chalk_1.default.gray(` Screenshot: ${result.artifacts.screenshotPath}`));
|
|
465
|
+
}
|
|
466
|
+
if (result.artifacts.previewFileUrl) {
|
|
467
|
+
console.log(chalk_1.default.gray(` File URL: `) + chalk_1.default.cyan.underline(result.artifacts.previewFileUrl));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
console.log();
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
spinner.stop();
|
|
474
|
+
this.logger.error(`Preview proof gate failed: ${error?.message || String(error)}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
exports.PreviewCommand = PreviewCommand;
|
package/dist/index.js
CHANGED
|
@@ -66,6 +66,7 @@ const repo_js_1 = require("./commands/repo.js");
|
|
|
66
66
|
const deploy_js_1 = require("./commands/deploy.js");
|
|
67
67
|
const bridge_js_1 = require("./commands/bridge.js");
|
|
68
68
|
const workflow_js_1 = require("./commands/workflow.js");
|
|
69
|
+
const preview_js_1 = require("./commands/preview.js");
|
|
69
70
|
const config_js_2 = require("./utils/config.js");
|
|
70
71
|
const logger_js_1 = require("./utils/logger.js");
|
|
71
72
|
const chalk_1 = __importDefault(require("chalk"));
|
|
@@ -661,6 +662,29 @@ async function main() {
|
|
|
661
662
|
project: options.project
|
|
662
663
|
});
|
|
663
664
|
});
|
|
665
|
+
// ==================== PREVIEW COMMAND ====================
|
|
666
|
+
program
|
|
667
|
+
.command('preview')
|
|
668
|
+
.description('Preview project locally with visual diffs and proof validation')
|
|
669
|
+
.option('-p, --project <path>', 'Project directory path', process.cwd())
|
|
670
|
+
.option('-e, --entry <file>', 'Entry HTML file (auto-detected if omitted)')
|
|
671
|
+
.option('--port <number>', 'Local server port', parseInt)
|
|
672
|
+
.option('--no-open', 'Do not auto-open browser')
|
|
673
|
+
.option('--diff', 'Show consolidated diff of recent agent changes')
|
|
674
|
+
.option('--proof', 'Run Template Service preview gate and persist proof bundle')
|
|
675
|
+
.option('--screenshot', 'Capture screenshot via Puppeteer')
|
|
676
|
+
.action(async (options) => {
|
|
677
|
+
const preview = new preview_js_1.PreviewCommand(config, logger);
|
|
678
|
+
await preview.run({
|
|
679
|
+
project: options.project,
|
|
680
|
+
entry: options.entry,
|
|
681
|
+
port: options.port,
|
|
682
|
+
open: options.open,
|
|
683
|
+
diff: options.diff,
|
|
684
|
+
proof: options.proof,
|
|
685
|
+
screenshot: options.screenshot,
|
|
686
|
+
});
|
|
687
|
+
});
|
|
664
688
|
// ==================== AUTH COMMANDS ====================
|
|
665
689
|
// Auth commands
|
|
666
690
|
program
|
package/dist/utils/api.d.ts
CHANGED
package/dist/utils/api.js
CHANGED
|
@@ -17,6 +17,7 @@ const https_1 = __importDefault(require("https"));
|
|
|
17
17
|
const net_1 = __importDefault(require("net"));
|
|
18
18
|
const path_1 = __importDefault(require("path"));
|
|
19
19
|
const ws_1 = __importDefault(require("ws"));
|
|
20
|
+
const workspace_stream_js_1 = require("./workspace-stream.js");
|
|
20
21
|
class CLIError extends Error {
|
|
21
22
|
category;
|
|
22
23
|
statusCode;
|
|
@@ -2636,6 +2637,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2636
2637
|
serverWorkspaceRoot = event.workspace_root.trim();
|
|
2637
2638
|
}
|
|
2638
2639
|
this.captureV3AgentStreamMutation(event, streamedFiles, serverWorkspaceRoot);
|
|
2640
|
+
// Real-time workspace streaming: apply file mutations to local disk immediately
|
|
2641
|
+
if (event.type === 'file_mutation') {
|
|
2642
|
+
const localRoot = context.projectPath || context.workspacePath || context.targetPath;
|
|
2643
|
+
if (localRoot && typeof event.path === 'string') {
|
|
2644
|
+
(0, workspace_stream_js_1.applyFileMutation)(event, localRoot);
|
|
2645
|
+
if (typeof event.content === 'string') {
|
|
2646
|
+
const relPath = this.normalizeAgentWorkspaceRelativePath(event.path, serverWorkspaceRoot || undefined);
|
|
2647
|
+
if (relPath) {
|
|
2648
|
+
streamedFiles[relPath] = event.content;
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2639
2653
|
// Empty workspace guard: if the remote agent lists its root
|
|
2640
2654
|
// and finds nothing while our local workspace has files, the
|
|
2641
2655
|
// workspace was not hydrated. Abort early with a clear error
|
|
@@ -2817,6 +2831,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2817
2831
|
contextId: finalContextId,
|
|
2818
2832
|
backendUrl: baseUrl,
|
|
2819
2833
|
partial: continuationData.checkpointed === true,
|
|
2834
|
+
changedFiles: continuationData.files || data.files || {},
|
|
2820
2835
|
metadata: { source: 'v3-agent', mode: 'agent', contextId: finalContextId, continuations, previewGate },
|
|
2821
2836
|
};
|
|
2822
2837
|
}
|
|
@@ -2835,6 +2850,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2835
2850
|
taskId: data.task_id || null,
|
|
2836
2851
|
contextId,
|
|
2837
2852
|
backendUrl: baseUrl,
|
|
2853
|
+
changedFiles: data.files || {},
|
|
2838
2854
|
metadata: { source: 'v3-agent', mode: 'agent', contextId, mcpContextId, previewGate },
|
|
2839
2855
|
};
|
|
2840
2856
|
}
|
|
@@ -2850,6 +2866,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2850
2866
|
contextId: error.partialData.context_id || requestExecutionContext.contextId || executionContext.contextId || null,
|
|
2851
2867
|
backendUrl: baseUrl,
|
|
2852
2868
|
partial: true,
|
|
2869
|
+
changedFiles: error.partialData.files || {},
|
|
2853
2870
|
metadata: { source: 'v3-agent', mode: 'agent', partial: true, contextId: error.partialData.context_id || requestExecutionContext.contextId || executionContext.contextId || null, mcpContextId: requestExecutionContext.mcpContextId || executionContext.mcpContextId || null, previewGate },
|
|
2854
2871
|
};
|
|
2855
2872
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Stream — Real-time bidirectional file sync between CLI and V3.
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* 1. WorkspaceWatcher — chokidar-based file watcher that detects local changes
|
|
6
|
+
* and pushes them to V3 via WebSocket.
|
|
7
|
+
* 2. MutationApplier — receives file_mutation events from V3's SSE stream
|
|
8
|
+
* and applies them to the local workspace IN REAL TIME (not at completion).
|
|
9
|
+
* 3. WorkspaceWSClient — WebSocket client connected to V3's /ws/workspace
|
|
10
|
+
* for bidirectional file sync outside the SSE stream.
|
|
11
|
+
*
|
|
12
|
+
* Event types (V3 → CLI via SSE):
|
|
13
|
+
* {"type": "file_mutation", "path": "rel/path", "content": "...", "action": "write"|"edit", "tool": "write_file"|"edit_file"}
|
|
14
|
+
*
|
|
15
|
+
* Event types (CLI → V3 via /ws/workspace):
|
|
16
|
+
* {"type": "bind", "context_id": "...", "workspace_root": "..."}
|
|
17
|
+
* {"type": "file_sync", "path": "rel/path", "content": "...", "action": "write"|"delete"}
|
|
18
|
+
* {"type": "file_batch", "files": [{path, content}, ...]}
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Apply a file_mutation event from V3's SSE stream to the local workspace.
|
|
22
|
+
* Call this from the SSE event handler for real-time file application.
|
|
23
|
+
*/
|
|
24
|
+
export declare function applyFileMutation(event: {
|
|
25
|
+
type: string;
|
|
26
|
+
path: string;
|
|
27
|
+
content: string;
|
|
28
|
+
action: string;
|
|
29
|
+
}, workspaceRoot: string): boolean;
|
|
30
|
+
export interface WatcherOptions {
|
|
31
|
+
workspaceRoot: string;
|
|
32
|
+
onFileChange?: (relativePath: string, content: string | null, action: 'write' | 'delete') => void;
|
|
33
|
+
}
|
|
34
|
+
export declare class WorkspaceWatcher {
|
|
35
|
+
private watcher;
|
|
36
|
+
private workspaceRoot;
|
|
37
|
+
private onFileChange;
|
|
38
|
+
private _ready;
|
|
39
|
+
constructor(options: WatcherOptions);
|
|
40
|
+
start(): void;
|
|
41
|
+
stop(): void;
|
|
42
|
+
get isReady(): boolean;
|
|
43
|
+
private _handleChange;
|
|
44
|
+
}
|
|
45
|
+
export interface WorkspaceWSOptions {
|
|
46
|
+
serverUrl: string;
|
|
47
|
+
token: string;
|
|
48
|
+
contextId: string;
|
|
49
|
+
workspaceRoot: string;
|
|
50
|
+
onMutation?: (event: any) => void;
|
|
51
|
+
}
|
|
52
|
+
export declare class WorkspaceWSClient {
|
|
53
|
+
private ws;
|
|
54
|
+
private opts;
|
|
55
|
+
private reconnectTimer;
|
|
56
|
+
private _connected;
|
|
57
|
+
private _queue;
|
|
58
|
+
private _maxQueue;
|
|
59
|
+
constructor(opts: WorkspaceWSOptions);
|
|
60
|
+
connect(): void;
|
|
61
|
+
disconnect(): void;
|
|
62
|
+
/**
|
|
63
|
+
* Push a local file change to V3's workspace.
|
|
64
|
+
*/
|
|
65
|
+
syncFile(relativePath: string, content: string | null, action: 'write' | 'delete'): void;
|
|
66
|
+
/**
|
|
67
|
+
* Push multiple files at once.
|
|
68
|
+
*/
|
|
69
|
+
syncBatch(files: Array<{
|
|
70
|
+
path: string;
|
|
71
|
+
content: string;
|
|
72
|
+
}>): void;
|
|
73
|
+
get isConnected(): boolean;
|
|
74
|
+
private _send;
|
|
75
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Workspace Stream — Real-time bidirectional file sync between CLI and V3.
|
|
4
|
+
*
|
|
5
|
+
* Architecture:
|
|
6
|
+
* 1. WorkspaceWatcher — chokidar-based file watcher that detects local changes
|
|
7
|
+
* and pushes them to V3 via WebSocket.
|
|
8
|
+
* 2. MutationApplier — receives file_mutation events from V3's SSE stream
|
|
9
|
+
* and applies them to the local workspace IN REAL TIME (not at completion).
|
|
10
|
+
* 3. WorkspaceWSClient — WebSocket client connected to V3's /ws/workspace
|
|
11
|
+
* for bidirectional file sync outside the SSE stream.
|
|
12
|
+
*
|
|
13
|
+
* Event types (V3 → CLI via SSE):
|
|
14
|
+
* {"type": "file_mutation", "path": "rel/path", "content": "...", "action": "write"|"edit", "tool": "write_file"|"edit_file"}
|
|
15
|
+
*
|
|
16
|
+
* Event types (CLI → V3 via /ws/workspace):
|
|
17
|
+
* {"type": "bind", "context_id": "...", "workspace_root": "..."}
|
|
18
|
+
* {"type": "file_sync", "path": "rel/path", "content": "...", "action": "write"|"delete"}
|
|
19
|
+
* {"type": "file_batch", "files": [{path, content}, ...]}
|
|
20
|
+
*/
|
|
21
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
24
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
25
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
26
|
+
}
|
|
27
|
+
Object.defineProperty(o, k2, desc);
|
|
28
|
+
}) : (function(o, m, k, k2) {
|
|
29
|
+
if (k2 === undefined) k2 = k;
|
|
30
|
+
o[k2] = m[k];
|
|
31
|
+
}));
|
|
32
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
33
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
34
|
+
}) : function(o, v) {
|
|
35
|
+
o["default"] = v;
|
|
36
|
+
});
|
|
37
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
38
|
+
var ownKeys = function(o) {
|
|
39
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
40
|
+
var ar = [];
|
|
41
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
42
|
+
return ar;
|
|
43
|
+
};
|
|
44
|
+
return ownKeys(o);
|
|
45
|
+
};
|
|
46
|
+
return function (mod) {
|
|
47
|
+
if (mod && mod.__esModule) return mod;
|
|
48
|
+
var result = {};
|
|
49
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
50
|
+
__setModuleDefault(result, mod);
|
|
51
|
+
return result;
|
|
52
|
+
};
|
|
53
|
+
})();
|
|
54
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
55
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
56
|
+
};
|
|
57
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
58
|
+
exports.WorkspaceWSClient = exports.WorkspaceWatcher = void 0;
|
|
59
|
+
exports.applyFileMutation = applyFileMutation;
|
|
60
|
+
const chokidar = __importStar(require("chokidar"));
|
|
61
|
+
const fs_1 = __importDefault(require("fs"));
|
|
62
|
+
const path_1 = __importDefault(require("path"));
|
|
63
|
+
const ws_1 = __importDefault(require("ws"));
|
|
64
|
+
const logger_js_1 = require("./logger.js");
|
|
65
|
+
const logger = new logger_js_1.Logger();
|
|
66
|
+
logger.setVerbose(!!process.env.VIGTHORIA_DEBUG);
|
|
67
|
+
// Files/dirs to ignore in the watcher
|
|
68
|
+
const IGNORE_PATTERNS = [
|
|
69
|
+
'**/node_modules/**',
|
|
70
|
+
'**/.git/**',
|
|
71
|
+
'**/__pycache__/**',
|
|
72
|
+
'**/.venv/**',
|
|
73
|
+
'**/dist/**',
|
|
74
|
+
'**/build/**',
|
|
75
|
+
'**/.next/**',
|
|
76
|
+
'**/.cache/**',
|
|
77
|
+
'**/.vigthoria/**',
|
|
78
|
+
'**/coverage/**',
|
|
79
|
+
'**/*.pyc',
|
|
80
|
+
];
|
|
81
|
+
const BINARY_EXTENSIONS = new Set([
|
|
82
|
+
'.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg', '.webp', '.bmp',
|
|
83
|
+
'.mp3', '.mp4', '.wav', '.ogg', '.webm', '.avi',
|
|
84
|
+
'.zip', '.tar', '.gz', '.bz2', '.7z', '.rar',
|
|
85
|
+
'.woff', '.woff2', '.ttf', '.eot', '.otf',
|
|
86
|
+
'.pdf', '.doc', '.docx', '.xls', '.xlsx',
|
|
87
|
+
'.pyc', '.pyo', '.so', '.dll', '.exe', '.bin',
|
|
88
|
+
'.db', '.sqlite', '.sqlite3',
|
|
89
|
+
]);
|
|
90
|
+
const MAX_SYNC_FILE_BYTES = 500 * 1024; // 500 KB
|
|
91
|
+
// ── Mutation Applier ─────────────────────────────────────────
|
|
92
|
+
/**
|
|
93
|
+
* Tracks files that V3 has mutated so the watcher doesn't echo them back.
|
|
94
|
+
*/
|
|
95
|
+
const _v3MutatedFiles = new Set();
|
|
96
|
+
const _v3MuteTimeout = 2000; // 2s mute window after V3 writes a file
|
|
97
|
+
/**
|
|
98
|
+
* Apply a file_mutation event from V3's SSE stream to the local workspace.
|
|
99
|
+
* Call this from the SSE event handler for real-time file application.
|
|
100
|
+
*/
|
|
101
|
+
function applyFileMutation(event, workspaceRoot) {
|
|
102
|
+
if (event.type !== 'file_mutation')
|
|
103
|
+
return false;
|
|
104
|
+
if (!event.path || !workspaceRoot)
|
|
105
|
+
return false;
|
|
106
|
+
const absPath = path_1.default.resolve(workspaceRoot, event.path);
|
|
107
|
+
// Safety: ensure the resolved path is within the workspace
|
|
108
|
+
if (!absPath.startsWith(path_1.default.resolve(workspaceRoot) + path_1.default.sep) && absPath !== path_1.default.resolve(workspaceRoot)) {
|
|
109
|
+
logger.warn(`Refusing to apply mutation outside workspace: ${event.path}`);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
if (event.action === 'delete') {
|
|
114
|
+
if (fs_1.default.existsSync(absPath)) {
|
|
115
|
+
fs_1.default.unlinkSync(absPath);
|
|
116
|
+
logger.debug(`Deleted: ${event.path}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else if (typeof event.content === 'string') {
|
|
120
|
+
fs_1.default.mkdirSync(path_1.default.dirname(absPath), { recursive: true });
|
|
121
|
+
fs_1.default.writeFileSync(absPath, event.content, 'utf8');
|
|
122
|
+
// Mute the watcher for this file to prevent echo
|
|
123
|
+
_v3MutatedFiles.add(absPath);
|
|
124
|
+
setTimeout(() => _v3MutatedFiles.delete(absPath), _v3MuteTimeout);
|
|
125
|
+
logger.debug(`Applied: ${event.path} (${event.action})`);
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
logger.error(`Failed to apply mutation for ${event.path}: ${err}`);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
class WorkspaceWatcher {
|
|
135
|
+
watcher = null;
|
|
136
|
+
workspaceRoot;
|
|
137
|
+
onFileChange;
|
|
138
|
+
_ready = false;
|
|
139
|
+
constructor(options) {
|
|
140
|
+
this.workspaceRoot = path_1.default.resolve(options.workspaceRoot);
|
|
141
|
+
this.onFileChange = options.onFileChange;
|
|
142
|
+
}
|
|
143
|
+
start() {
|
|
144
|
+
if (this.watcher)
|
|
145
|
+
return;
|
|
146
|
+
this.watcher = chokidar.watch(this.workspaceRoot, {
|
|
147
|
+
ignored: IGNORE_PATTERNS,
|
|
148
|
+
persistent: true,
|
|
149
|
+
ignoreInitial: true,
|
|
150
|
+
awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 100 },
|
|
151
|
+
});
|
|
152
|
+
this.watcher
|
|
153
|
+
.on('ready', () => {
|
|
154
|
+
this._ready = true;
|
|
155
|
+
logger.debug('Workspace watcher ready');
|
|
156
|
+
})
|
|
157
|
+
.on('add', (filePath) => this._handleChange(filePath, 'write'))
|
|
158
|
+
.on('change', (filePath) => this._handleChange(filePath, 'write'))
|
|
159
|
+
.on('unlink', (filePath) => this._handleChange(filePath, 'delete'));
|
|
160
|
+
}
|
|
161
|
+
stop() {
|
|
162
|
+
if (this.watcher) {
|
|
163
|
+
this.watcher.close();
|
|
164
|
+
this.watcher = null;
|
|
165
|
+
this._ready = false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
get isReady() {
|
|
169
|
+
return this._ready;
|
|
170
|
+
}
|
|
171
|
+
_handleChange(filePath, action) {
|
|
172
|
+
// Skip if this file was just written by V3 (echo prevention)
|
|
173
|
+
if (_v3MutatedFiles.has(filePath))
|
|
174
|
+
return;
|
|
175
|
+
const ext = path_1.default.extname(filePath).toLowerCase();
|
|
176
|
+
if (BINARY_EXTENSIONS.has(ext))
|
|
177
|
+
return;
|
|
178
|
+
const relativePath = path_1.default.relative(this.workspaceRoot, filePath);
|
|
179
|
+
if (relativePath.startsWith('..'))
|
|
180
|
+
return; // safety
|
|
181
|
+
if (action === 'delete') {
|
|
182
|
+
this.onFileChange?.(relativePath, null, 'delete');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const stat = fs_1.default.statSync(filePath);
|
|
187
|
+
if (stat.size > MAX_SYNC_FILE_BYTES)
|
|
188
|
+
return;
|
|
189
|
+
const content = fs_1.default.readFileSync(filePath, 'utf8');
|
|
190
|
+
this.onFileChange?.(relativePath, content, 'write');
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// File might have been deleted between event and read
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
exports.WorkspaceWatcher = WorkspaceWatcher;
|
|
198
|
+
class WorkspaceWSClient {
|
|
199
|
+
ws = null;
|
|
200
|
+
opts;
|
|
201
|
+
reconnectTimer = null;
|
|
202
|
+
_connected = false;
|
|
203
|
+
_queue = [];
|
|
204
|
+
_maxQueue = 200;
|
|
205
|
+
constructor(opts) {
|
|
206
|
+
this.opts = opts;
|
|
207
|
+
}
|
|
208
|
+
connect() {
|
|
209
|
+
if (this.ws)
|
|
210
|
+
return;
|
|
211
|
+
const url = `${this.opts.serverUrl}/ws/workspace?token=${encodeURIComponent(this.opts.token)}`;
|
|
212
|
+
this.ws = new ws_1.default(url);
|
|
213
|
+
this.ws.on('open', () => {
|
|
214
|
+
this._connected = true;
|
|
215
|
+
// Bind to workspace
|
|
216
|
+
this._send({
|
|
217
|
+
type: 'bind',
|
|
218
|
+
context_id: this.opts.contextId,
|
|
219
|
+
workspace_root: this.opts.workspaceRoot,
|
|
220
|
+
});
|
|
221
|
+
// Flush queued messages
|
|
222
|
+
while (this._queue.length > 0) {
|
|
223
|
+
const msg = this._queue.shift();
|
|
224
|
+
this._send(msg);
|
|
225
|
+
}
|
|
226
|
+
logger.debug('WS workspace connected');
|
|
227
|
+
});
|
|
228
|
+
this.ws.on('message', (data) => {
|
|
229
|
+
try {
|
|
230
|
+
const event = JSON.parse(data.toString());
|
|
231
|
+
if (event.type === 'file_mutation') {
|
|
232
|
+
this.opts.onMutation?.(event);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// ignore parse errors
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
this.ws.on('close', () => {
|
|
240
|
+
this._connected = false;
|
|
241
|
+
this.ws = null;
|
|
242
|
+
// Auto-reconnect after 3s
|
|
243
|
+
this.reconnectTimer = setTimeout(() => this.connect(), 3000);
|
|
244
|
+
});
|
|
245
|
+
this.ws.on('error', () => {
|
|
246
|
+
// error handler to prevent crash, close event handles reconnect
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
disconnect() {
|
|
250
|
+
if (this.reconnectTimer) {
|
|
251
|
+
clearTimeout(this.reconnectTimer);
|
|
252
|
+
this.reconnectTimer = null;
|
|
253
|
+
}
|
|
254
|
+
if (this.ws) {
|
|
255
|
+
this.ws.close();
|
|
256
|
+
this.ws = null;
|
|
257
|
+
}
|
|
258
|
+
this._connected = false;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Push a local file change to V3's workspace.
|
|
262
|
+
*/
|
|
263
|
+
syncFile(relativePath, content, action) {
|
|
264
|
+
const msg = {
|
|
265
|
+
type: 'file_sync',
|
|
266
|
+
path: relativePath,
|
|
267
|
+
content: content || '',
|
|
268
|
+
action,
|
|
269
|
+
};
|
|
270
|
+
if (this._connected) {
|
|
271
|
+
this._send(msg);
|
|
272
|
+
}
|
|
273
|
+
else if (this._queue.length < this._maxQueue) {
|
|
274
|
+
this._queue.push(msg);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Push multiple files at once.
|
|
279
|
+
*/
|
|
280
|
+
syncBatch(files) {
|
|
281
|
+
const msg = { type: 'file_batch', files };
|
|
282
|
+
if (this._connected) {
|
|
283
|
+
this._send(msg);
|
|
284
|
+
}
|
|
285
|
+
else if (this._queue.length < this._maxQueue) {
|
|
286
|
+
this._queue.push(msg);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
get isConnected() {
|
|
290
|
+
return this._connected;
|
|
291
|
+
}
|
|
292
|
+
_send(msg) {
|
|
293
|
+
try {
|
|
294
|
+
this.ws?.send(JSON.stringify(msg));
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// connection lost
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
exports.WorkspaceWSClient = WorkspaceWSClient;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigthoria-cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.55",
|
|
4
4
|
"description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"archiver": "^6.0.1",
|
|
62
62
|
"axios": "^1.6.0",
|
|
63
63
|
"chalk": "^5.3.0",
|
|
64
|
+
"chokidar": "^5.0.0",
|
|
64
65
|
"commander": "^11.1.0",
|
|
65
66
|
"conf": "^12.0.0",
|
|
66
67
|
"diff": "^5.1.0",
|
|
@@ -86,4 +87,4 @@
|
|
|
86
87
|
"engines": {
|
|
87
88
|
"node": ">=18.0.0"
|
|
88
89
|
}
|
|
89
|
-
}
|
|
90
|
+
}
|