recoder-code 2.5.2 → 2.5.3
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/index.js +0 -0
- package/dist/src/commands/context/index.js +2 -2
- package/dist/src/commands/mcp/marketplace.d.ts +6 -0
- package/dist/src/commands/mcp/marketplace.js +448 -0
- package/dist/src/commands/mcp.js +2 -0
- package/dist/src/commands/parallel.d.ts +20 -0
- package/dist/src/commands/parallel.js +133 -0
- package/dist/src/commands/recoderWeb.js +184 -5
- package/dist/src/commands/web/diff.d.ts +13 -0
- package/dist/src/commands/web/diff.js +235 -0
- package/dist/src/commands/web/link.d.ts +11 -0
- package/dist/src/commands/web/link.js +96 -0
- package/dist/src/commands/web/pull.d.ts +13 -0
- package/dist/src/commands/web/pull.js +203 -0
- package/dist/src/commands/web/status.d.ts +10 -0
- package/dist/src/commands/web/status.js +104 -0
- package/dist/src/commands/web/unlink.d.ts +10 -0
- package/dist/src/commands/web/unlink.js +45 -0
- package/dist/src/commands/web/watch.d.ts +14 -0
- package/dist/src/commands/web/watch.js +360 -0
- package/dist/src/commands/web.js +12 -0
- package/dist/src/config/config.js +6 -2
- package/dist/src/config/defaultMcpServers.d.ts +1 -0
- package/dist/src/config/defaultMcpServers.js +46 -0
- package/dist/src/gemini.js +10 -0
- package/dist/src/parallel/git-utils.d.ts +42 -0
- package/dist/src/parallel/git-utils.js +161 -0
- package/dist/src/parallel/index.d.ts +14 -0
- package/dist/src/parallel/index.js +14 -0
- package/dist/src/parallel/parallel-mode.d.ts +48 -0
- package/dist/src/parallel/parallel-mode.js +224 -0
- package/dist/src/services/AgentBridgeService.d.ts +61 -0
- package/dist/src/services/AgentBridgeService.js +253 -0
- package/dist/src/services/BuiltinCommandLoader.js +7 -0
- package/dist/src/services/PlatformSyncService.d.ts +154 -0
- package/dist/src/services/PlatformSyncService.js +588 -0
- package/dist/src/ui/commands/workflowCommands.d.ts +16 -0
- package/dist/src/ui/commands/workflowCommands.js +291 -0
- package/dist/src/ui/commands/workspaceCommand.d.ts +11 -0
- package/dist/src/ui/commands/workspaceCommand.js +329 -0
- package/dist/src/zed-integration/schema.d.ts +30 -30
- package/package.json +29 -10
- package/src/postinstall.cjs +3 -2
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform Sync Service
|
|
3
|
+
* Handles bidirectional sync between recoder-code CLI and recoder.xyz web platform
|
|
4
|
+
* Detects if running inside recoder.xyz container and auto-connects
|
|
5
|
+
*/
|
|
6
|
+
import WebSocket from 'ws';
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
const DOCKER_BACKEND_URL = process.env['DOCKER_BACKEND_URL'] || 'https://docker.recoder.xyz';
|
|
11
|
+
const RECODER_WEB_URL = process.env['RECODER_WEB_URL'] || 'https://web.recoder.xyz';
|
|
12
|
+
export class PlatformSyncService extends EventEmitter {
|
|
13
|
+
ws = null;
|
|
14
|
+
reconnectTimeout = null;
|
|
15
|
+
platformInfo = null;
|
|
16
|
+
connected = false;
|
|
17
|
+
fileWatcher = null;
|
|
18
|
+
pendingChanges = [];
|
|
19
|
+
syncEnabled = true;
|
|
20
|
+
/**
|
|
21
|
+
* Detect platform environment (container or local linked project)
|
|
22
|
+
*/
|
|
23
|
+
async detectPlatform(workingDir) {
|
|
24
|
+
const cwd = workingDir || process.cwd();
|
|
25
|
+
// Check for container markers first
|
|
26
|
+
const isContainer = this.isInRecoderContainer();
|
|
27
|
+
if (isContainer) {
|
|
28
|
+
// Running in container
|
|
29
|
+
const projectId = this.detectProjectId();
|
|
30
|
+
const previewUrl = await this.detectPreviewUrl();
|
|
31
|
+
this.platformInfo = {
|
|
32
|
+
isContainer: true,
|
|
33
|
+
isLinked: true,
|
|
34
|
+
projectId,
|
|
35
|
+
previewUrl,
|
|
36
|
+
backendUrl: DOCKER_BACKEND_URL,
|
|
37
|
+
webUrl: RECODER_WEB_URL,
|
|
38
|
+
workingDir: '/workspace',
|
|
39
|
+
};
|
|
40
|
+
return this.platformInfo;
|
|
41
|
+
}
|
|
42
|
+
// Check for local linked project (.recoder-web file)
|
|
43
|
+
const linkedProject = this.detectLinkedProject(cwd);
|
|
44
|
+
if (linkedProject) {
|
|
45
|
+
this.platformInfo = {
|
|
46
|
+
isContainer: false,
|
|
47
|
+
isLinked: true,
|
|
48
|
+
projectId: linkedProject.projectId,
|
|
49
|
+
previewUrl: linkedProject.previewUrl,
|
|
50
|
+
backendUrl: DOCKER_BACKEND_URL,
|
|
51
|
+
webUrl: linkedProject.webUrl || RECODER_WEB_URL,
|
|
52
|
+
workingDir: cwd,
|
|
53
|
+
};
|
|
54
|
+
return this.platformInfo;
|
|
55
|
+
}
|
|
56
|
+
// Not in container and not linked
|
|
57
|
+
this.platformInfo = { isContainer: false, isLinked: false };
|
|
58
|
+
return this.platformInfo;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Detect linked project from .recoder-web file
|
|
62
|
+
*/
|
|
63
|
+
detectLinkedProject(dir) {
|
|
64
|
+
try {
|
|
65
|
+
const metaFile = path.join(dir, '.recoder-web');
|
|
66
|
+
if (fs.existsSync(metaFile)) {
|
|
67
|
+
const meta = JSON.parse(fs.readFileSync(metaFile, 'utf-8'));
|
|
68
|
+
const projectId = meta.urlId || meta.projectId;
|
|
69
|
+
if (projectId) {
|
|
70
|
+
return {
|
|
71
|
+
projectId,
|
|
72
|
+
previewUrl: meta.previewUrl || `${DOCKER_BACKEND_URL}/preview/${projectId}`,
|
|
73
|
+
webUrl: meta.webUrl || `${RECODER_WEB_URL}/chat/${projectId}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Ignore
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Check if we're inside a recoder container
|
|
85
|
+
*/
|
|
86
|
+
isInRecoderContainer() {
|
|
87
|
+
// Check for container-specific markers
|
|
88
|
+
const markers = [
|
|
89
|
+
// Check if /workspace exists (our standard working directory)
|
|
90
|
+
fs.existsSync('/workspace'),
|
|
91
|
+
// Check for recoder environment variable
|
|
92
|
+
process.env['RECODER_CONTAINER'] === 'true',
|
|
93
|
+
// Check for PTY_PORT (our container's PTY server)
|
|
94
|
+
!!process.env['PTY_PORT'],
|
|
95
|
+
// Check if running in Docker
|
|
96
|
+
fs.existsSync('/.dockerenv'),
|
|
97
|
+
];
|
|
98
|
+
return markers.some(Boolean);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Extract project ID from container context
|
|
102
|
+
*/
|
|
103
|
+
detectProjectId() {
|
|
104
|
+
// Try environment variable first
|
|
105
|
+
if (process.env['RECODER_PROJECT_ID']) {
|
|
106
|
+
return process.env['RECODER_PROJECT_ID'];
|
|
107
|
+
}
|
|
108
|
+
// Try to read from .recoder-web file
|
|
109
|
+
try {
|
|
110
|
+
const metaFile = path.join('/workspace', '.recoder-web');
|
|
111
|
+
if (fs.existsSync(metaFile)) {
|
|
112
|
+
const meta = JSON.parse(fs.readFileSync(metaFile, 'utf-8'));
|
|
113
|
+
return meta.projectId || meta.urlId;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Ignore
|
|
118
|
+
}
|
|
119
|
+
// Try to extract from volume name (container labels)
|
|
120
|
+
// This is set by docker-backend when creating containers
|
|
121
|
+
try {
|
|
122
|
+
const hostname = fs.readFileSync('/etc/hostname', 'utf-8').trim();
|
|
123
|
+
if (hostname.startsWith('recoder-')) {
|
|
124
|
+
return hostname.replace('recoder-', '');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Ignore
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Get preview URL - constructed from env vars
|
|
134
|
+
* Preview is proxied through docker-backend at /preview/{projectId}
|
|
135
|
+
*/
|
|
136
|
+
async detectPreviewUrl() {
|
|
137
|
+
const projectId = process.env['RECODER_PROJECT_ID'];
|
|
138
|
+
if (!projectId)
|
|
139
|
+
return undefined;
|
|
140
|
+
// Preview is accessible via docker-backend proxy
|
|
141
|
+
return `${DOCKER_BACKEND_URL}/preview/${projectId}`;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Auto-initialize when running in container or linked project
|
|
145
|
+
* Call this on CLI startup
|
|
146
|
+
*/
|
|
147
|
+
async autoInitialize() {
|
|
148
|
+
const platform = await this.detectPlatform();
|
|
149
|
+
// Not connected to platform at all
|
|
150
|
+
if (!platform.isContainer && !platform.isLinked) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Show appropriate header
|
|
154
|
+
if (platform.isContainer) {
|
|
155
|
+
console.log('\n┌────────────────────────────────────────────────┐');
|
|
156
|
+
console.log('│ 🐳 recoder.xyz Container Environment │');
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
console.log('\n┌────────────────────────────────────────────────┐');
|
|
160
|
+
console.log('│ 🔗 recoder.xyz Linked Project │');
|
|
161
|
+
}
|
|
162
|
+
console.log('├────────────────────────────────────────────────┤');
|
|
163
|
+
console.log(`│ 📁 Project: ${(platform.projectId || 'Unknown').padEnd(33)}│`);
|
|
164
|
+
if (platform.previewUrl) {
|
|
165
|
+
console.log(`│ 🌐 Preview: ${platform.previewUrl.substring(0, 33).padEnd(33)}│`);
|
|
166
|
+
}
|
|
167
|
+
const webUrl = this.getWebProjectUrl();
|
|
168
|
+
if (webUrl) {
|
|
169
|
+
console.log(`│ 💻 Web IDE: ${webUrl.substring(0, 33).padEnd(33)}│`);
|
|
170
|
+
}
|
|
171
|
+
// Auto-connect to sync (only for containers, local projects sync manually)
|
|
172
|
+
if (platform.isContainer) {
|
|
173
|
+
const connected = await this.connect();
|
|
174
|
+
if (connected) {
|
|
175
|
+
console.log('│ 📡 Sync: Connected │');
|
|
176
|
+
this.startFileWatcher();
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
console.log('│ 📡 Sync: Offline (changes queued) │');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
console.log('│ 📡 Sync: Manual (use `recoder web sync`) │');
|
|
184
|
+
}
|
|
185
|
+
console.log('└────────────────────────────────────────────────┘');
|
|
186
|
+
console.log('');
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Connect to platform sync WebSocket
|
|
190
|
+
*/
|
|
191
|
+
async connect() {
|
|
192
|
+
const platform = await this.detectPlatform();
|
|
193
|
+
if (!platform.isContainer) {
|
|
194
|
+
console.log('📍 Not running in recoder.xyz container');
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
if (!platform.projectId) {
|
|
198
|
+
console.log('⚠️ Container detected but no project ID found');
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
return this.connectWebSocket(platform.projectId);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Connect to file sync WebSocket
|
|
205
|
+
*/
|
|
206
|
+
connectWebSocket(projectId) {
|
|
207
|
+
return new Promise((resolve) => {
|
|
208
|
+
const wsUrl = DOCKER_BACKEND_URL.replace('http', 'ws');
|
|
209
|
+
try {
|
|
210
|
+
this.ws = new WebSocket(`${wsUrl}/sync/${projectId}`);
|
|
211
|
+
this.ws.on('open', () => {
|
|
212
|
+
console.log('🔗 Connected to recoder.xyz platform sync');
|
|
213
|
+
this.connected = true;
|
|
214
|
+
this.emit('connected');
|
|
215
|
+
// Flush any queued offline changes
|
|
216
|
+
this.flushPendingChanges();
|
|
217
|
+
resolve(true);
|
|
218
|
+
});
|
|
219
|
+
this.ws.on('message', (data) => {
|
|
220
|
+
try {
|
|
221
|
+
const msg = JSON.parse(data.toString());
|
|
222
|
+
this.handleMessage(msg);
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
// Ignore parse errors
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
this.ws.on('close', () => {
|
|
229
|
+
console.log('🔌 Platform sync disconnected');
|
|
230
|
+
this.connected = false;
|
|
231
|
+
this.emit('disconnected');
|
|
232
|
+
this.scheduleReconnect(projectId);
|
|
233
|
+
});
|
|
234
|
+
this.ws.on('error', (err) => {
|
|
235
|
+
console.error('❌ Platform sync error:', err.message);
|
|
236
|
+
this.connected = false;
|
|
237
|
+
resolve(false);
|
|
238
|
+
});
|
|
239
|
+
// Timeout after 5 seconds
|
|
240
|
+
setTimeout(() => {
|
|
241
|
+
if (!this.connected) {
|
|
242
|
+
resolve(false);
|
|
243
|
+
}
|
|
244
|
+
}, 5000);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
console.error('❌ Failed to connect:', error);
|
|
248
|
+
resolve(false);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Handle incoming sync messages
|
|
254
|
+
*/
|
|
255
|
+
handleMessage(msg) {
|
|
256
|
+
switch (msg.action) {
|
|
257
|
+
case 'file-changed':
|
|
258
|
+
// Remote file changed in web editor - check for conflicts
|
|
259
|
+
const changes = msg.changes;
|
|
260
|
+
for (const change of changes) {
|
|
261
|
+
if (change.content && this.detectConflict(change.path, change.content)) {
|
|
262
|
+
// Emit conflict for CLI to handle
|
|
263
|
+
try {
|
|
264
|
+
const fullPath = path.join('/workspace', change.path);
|
|
265
|
+
const localContent = fs.readFileSync(fullPath, 'utf-8');
|
|
266
|
+
this.emit('conflict', {
|
|
267
|
+
path: change.path,
|
|
268
|
+
localContent,
|
|
269
|
+
remoteContent: change.content,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
// If can't read local, just apply remote
|
|
274
|
+
this.emit('remoteChange', [change]);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
this.emit('remoteChange', [change]);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
break;
|
|
282
|
+
case 'refresh-preview':
|
|
283
|
+
// Web editor requested preview refresh
|
|
284
|
+
this.emit('refreshPreview');
|
|
285
|
+
break;
|
|
286
|
+
case 'content':
|
|
287
|
+
// File content received - track version
|
|
288
|
+
this.fileVersions.set(msg.path, {
|
|
289
|
+
content: msg.content,
|
|
290
|
+
timestamp: Date.now(),
|
|
291
|
+
});
|
|
292
|
+
this.emit('fileContent', { path: msg.path, content: msg.content });
|
|
293
|
+
break;
|
|
294
|
+
case 'files':
|
|
295
|
+
// File list received
|
|
296
|
+
this.emit('fileList', msg.files);
|
|
297
|
+
break;
|
|
298
|
+
case 'error':
|
|
299
|
+
console.error('Sync error:', msg.error);
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Schedule reconnection attempt
|
|
305
|
+
*/
|
|
306
|
+
scheduleReconnect(projectId) {
|
|
307
|
+
if (this.reconnectTimeout) {
|
|
308
|
+
clearTimeout(this.reconnectTimeout);
|
|
309
|
+
}
|
|
310
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
311
|
+
console.log('🔄 Attempting to reconnect...');
|
|
312
|
+
this.connectWebSocket(projectId);
|
|
313
|
+
}, 3000);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Send file change to platform (with offline queuing)
|
|
317
|
+
*/
|
|
318
|
+
async notifyFileChange(change) {
|
|
319
|
+
if (!this.syncEnabled)
|
|
320
|
+
return;
|
|
321
|
+
// Read file content for create/modify
|
|
322
|
+
if (change.type !== 'delete' && !change.content) {
|
|
323
|
+
try {
|
|
324
|
+
const fullPath = path.join('/workspace', change.path);
|
|
325
|
+
change.content = fs.readFileSync(fullPath, 'utf-8');
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// File might not exist yet
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// If offline, queue the change for later sync
|
|
332
|
+
if (!this.connected || !this.ws) {
|
|
333
|
+
this.pendingChanges.push(change);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
this.ws.send(JSON.stringify({
|
|
337
|
+
action: 'write',
|
|
338
|
+
path: change.path,
|
|
339
|
+
content: change.content || '',
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Flush pending changes when reconnected
|
|
344
|
+
*/
|
|
345
|
+
async flushPendingChanges() {
|
|
346
|
+
if (!this.connected || !this.ws || this.pendingChanges.length === 0)
|
|
347
|
+
return;
|
|
348
|
+
console.log(`📤 Syncing ${this.pendingChanges.length} offline changes...`);
|
|
349
|
+
for (const change of this.pendingChanges) {
|
|
350
|
+
this.ws.send(JSON.stringify({
|
|
351
|
+
action: 'write',
|
|
352
|
+
path: change.path,
|
|
353
|
+
content: change.content || '',
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
this.pendingChanges = [];
|
|
357
|
+
console.log('✅ Offline changes synced');
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Get count of pending offline changes
|
|
361
|
+
*/
|
|
362
|
+
getPendingChangesCount() {
|
|
363
|
+
return this.pendingChanges.length;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Track file versions for conflict detection
|
|
367
|
+
*/
|
|
368
|
+
fileVersions = new Map();
|
|
369
|
+
/**
|
|
370
|
+
* Check if a remote change conflicts with local changes
|
|
371
|
+
*/
|
|
372
|
+
detectConflict(remotePath, remoteContent) {
|
|
373
|
+
const local = this.fileVersions.get(remotePath);
|
|
374
|
+
if (!local)
|
|
375
|
+
return false;
|
|
376
|
+
// Check if local file was modified after last sync
|
|
377
|
+
try {
|
|
378
|
+
const fullPath = path.join('/workspace', remotePath);
|
|
379
|
+
const currentContent = fs.readFileSync(fullPath, 'utf-8');
|
|
380
|
+
// Conflict if local content differs from both synced version and remote
|
|
381
|
+
if (currentContent !== local.content && currentContent !== remoteContent) {
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Handle conflict resolution
|
|
392
|
+
* Returns: 'local' | 'remote' | 'merge' | 'skip'
|
|
393
|
+
*/
|
|
394
|
+
async resolveConflict(filePath, localContent, remoteContent, callback) {
|
|
395
|
+
// Emit conflict event for UI to handle
|
|
396
|
+
this.emit('conflict', {
|
|
397
|
+
path: filePath,
|
|
398
|
+
localContent,
|
|
399
|
+
remoteContent,
|
|
400
|
+
resolve: callback,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Start watching local files for changes
|
|
405
|
+
*/
|
|
406
|
+
startFileWatcher() {
|
|
407
|
+
if (this.fileWatcher || !this.platformInfo?.isContainer) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const workspaceDir = '/workspace';
|
|
411
|
+
const ignoredDirs = ['node_modules', '.git', 'dist', 'build', '.next'];
|
|
412
|
+
console.log('👁️ Started watching files for sync...');
|
|
413
|
+
// Use recursive file watching
|
|
414
|
+
this.fileWatcher = fs.watch(workspaceDir, { recursive: true }, (eventType, filename) => {
|
|
415
|
+
if (!filename)
|
|
416
|
+
return;
|
|
417
|
+
// Skip ignored directories
|
|
418
|
+
if (ignoredDirs.some(dir => filename.startsWith(dir + '/'))) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const fullPath = path.join(workspaceDir, filename);
|
|
422
|
+
const relativePath = filename;
|
|
423
|
+
// Debounce and determine change type
|
|
424
|
+
const changeType = fs.existsSync(fullPath) ?
|
|
425
|
+
(eventType === 'rename' ? 'create' : 'modify') :
|
|
426
|
+
'delete';
|
|
427
|
+
const change = {
|
|
428
|
+
type: changeType,
|
|
429
|
+
path: relativePath,
|
|
430
|
+
timestamp: Date.now(),
|
|
431
|
+
};
|
|
432
|
+
this.notifyFileChange(change);
|
|
433
|
+
this.emit('localChange', change);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Stop file watcher
|
|
438
|
+
*/
|
|
439
|
+
stopFileWatcher() {
|
|
440
|
+
if (this.fileWatcher) {
|
|
441
|
+
this.fileWatcher.close();
|
|
442
|
+
this.fileWatcher = null;
|
|
443
|
+
console.log('👁️ Stopped file watcher');
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Request file from platform
|
|
448
|
+
*/
|
|
449
|
+
async requestFile(filePath) {
|
|
450
|
+
if (!this.connected || !this.ws) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
this.ws.send(JSON.stringify({
|
|
454
|
+
action: 'read',
|
|
455
|
+
path: filePath,
|
|
456
|
+
}));
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Request file list from platform
|
|
460
|
+
*/
|
|
461
|
+
async requestFileList(directory = '/workspace') {
|
|
462
|
+
if (!this.connected || !this.ws) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
this.ws.send(JSON.stringify({
|
|
466
|
+
action: 'list',
|
|
467
|
+
path: directory,
|
|
468
|
+
}));
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Delete file via platform
|
|
472
|
+
*/
|
|
473
|
+
async deleteFile(filePath) {
|
|
474
|
+
if (!this.connected || !this.ws) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
this.ws.send(JSON.stringify({
|
|
478
|
+
action: 'delete',
|
|
479
|
+
path: filePath,
|
|
480
|
+
}));
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Disconnect from platform
|
|
484
|
+
*/
|
|
485
|
+
disconnect() {
|
|
486
|
+
if (this.reconnectTimeout) {
|
|
487
|
+
clearTimeout(this.reconnectTimeout);
|
|
488
|
+
this.reconnectTimeout = null;
|
|
489
|
+
}
|
|
490
|
+
if (this.ws) {
|
|
491
|
+
this.ws.close();
|
|
492
|
+
this.ws = null;
|
|
493
|
+
}
|
|
494
|
+
this.stopFileWatcher();
|
|
495
|
+
this.connected = false;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Get connection status
|
|
499
|
+
*/
|
|
500
|
+
isConnected() {
|
|
501
|
+
return this.connected;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Get platform info
|
|
505
|
+
*/
|
|
506
|
+
getPlatformInfo() {
|
|
507
|
+
return this.platformInfo;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Enable/disable sync
|
|
511
|
+
*/
|
|
512
|
+
setSyncEnabled(enabled) {
|
|
513
|
+
this.syncEnabled = enabled;
|
|
514
|
+
console.log(`📡 Sync ${enabled ? 'enabled' : 'disabled'}`);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Display platform status
|
|
518
|
+
*/
|
|
519
|
+
displayStatus() {
|
|
520
|
+
const info = this.platformInfo;
|
|
521
|
+
if (!info?.isContainer) {
|
|
522
|
+
console.log('📍 Running locally (not in recoder.xyz container)');
|
|
523
|
+
console.log('💡 Run in recoder.xyz terminal for live preview sync');
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
console.log('🐳 Running in recoder.xyz container');
|
|
527
|
+
console.log(`📁 Project: ${info.projectId || 'Unknown'}`);
|
|
528
|
+
console.log(`🔗 Backend: ${info.backendUrl}`);
|
|
529
|
+
if (info.previewUrl) {
|
|
530
|
+
console.log(`🌐 Preview: ${info.previewUrl}`);
|
|
531
|
+
}
|
|
532
|
+
console.log(`📡 Sync: ${this.connected ? 'Connected' : 'Disconnected'}`);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Save chat message to platform (for CLI-web chat sync)
|
|
536
|
+
*/
|
|
537
|
+
async saveChatMessage(message) {
|
|
538
|
+
if (!this.platformInfo?.projectId)
|
|
539
|
+
return;
|
|
540
|
+
try {
|
|
541
|
+
const response = await fetch(`${RECODER_WEB_URL}/api/chat-history`, {
|
|
542
|
+
method: 'POST',
|
|
543
|
+
headers: { 'Content-Type': 'application/json' },
|
|
544
|
+
body: JSON.stringify({
|
|
545
|
+
urlId: this.platformInfo.projectId,
|
|
546
|
+
projectId: this.platformInfo.projectId,
|
|
547
|
+
messages: [message],
|
|
548
|
+
source: 'cli',
|
|
549
|
+
cliVersion: process.env['npm_package_version'] || '2.5.0',
|
|
550
|
+
}),
|
|
551
|
+
});
|
|
552
|
+
if (!response.ok) {
|
|
553
|
+
console.warn('Failed to sync chat to platform');
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
catch (e) {
|
|
557
|
+
// Silently ignore - chat sync is optional
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Load chat history from platform
|
|
562
|
+
*/
|
|
563
|
+
async loadChatHistory() {
|
|
564
|
+
if (!this.platformInfo?.projectId)
|
|
565
|
+
return [];
|
|
566
|
+
try {
|
|
567
|
+
const response = await fetch(`${RECODER_WEB_URL}/api/chat-history?urlId=${this.platformInfo.projectId}`);
|
|
568
|
+
if (response.ok) {
|
|
569
|
+
const data = await response.json();
|
|
570
|
+
return data.data?.[0]?.messages || [];
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
catch (e) {
|
|
574
|
+
// Silently ignore
|
|
575
|
+
}
|
|
576
|
+
return [];
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Get the web URL for the current project
|
|
580
|
+
*/
|
|
581
|
+
getWebProjectUrl() {
|
|
582
|
+
if (!this.platformInfo?.projectId)
|
|
583
|
+
return undefined;
|
|
584
|
+
return `${RECODER_WEB_URL}/chat/${this.platformInfo.projectId}`;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
// Singleton instance
|
|
588
|
+
export const platformSync = new PlatformSyncService();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow & utility CLI commands for Recoder Code
|
|
3
|
+
*
|
|
4
|
+
* Slash commands:
|
|
5
|
+
* /workflow list - List workflows for current project
|
|
6
|
+
* /workflow run <id> - Execute a workflow
|
|
7
|
+
* /workflow status <exec-id> - Check execution status
|
|
8
|
+
* /analytics - Show agent analytics summary
|
|
9
|
+
* /cost - Show cost summary
|
|
10
|
+
* /whoami - Show current user info
|
|
11
|
+
*/
|
|
12
|
+
import type { SlashCommand } from './types.js';
|
|
13
|
+
export declare const workflowCommand: SlashCommand;
|
|
14
|
+
export declare const analyticsCommand: SlashCommand;
|
|
15
|
+
export declare const costCommand: SlashCommand;
|
|
16
|
+
export declare const whoamiCommand: SlashCommand;
|