nstantpage-agent 0.2.0

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.
@@ -0,0 +1,555 @@
1
+ /**
2
+ * Local Server — handles /live/* API requests on the user's machine.
3
+ *
4
+ * This replaces the PreviewGateway + Docker container workflow:
5
+ * - File sync (write files, track changes)
6
+ * - Type checking (run tsc locally)
7
+ * - Error tracking (build, type, runtime)
8
+ * - Package installation (npm/pnpm install)
9
+ * - Dev server management (start, stop, restart)
10
+ * - Terminal execution (run commands in project dir)
11
+ * - Logs and stats
12
+ *
13
+ * Requests arrive through the tunnel from the gateway.
14
+ */
15
+ import http from 'http';
16
+ import os from 'os';
17
+ import { DevServer } from './devServer.js';
18
+ import { FileManager } from './fileManager.js';
19
+ import { Checker } from './checker.js';
20
+ import { ErrorStore, structuredErrorToString } from './errorStore.js';
21
+ import { PackageInstaller } from './packageInstaller.js';
22
+ export class LocalServer {
23
+ server = null;
24
+ options;
25
+ devServer;
26
+ fileManager;
27
+ checker;
28
+ errorStore;
29
+ packageInstaller;
30
+ lastHeartbeat = Date.now();
31
+ constructor(options) {
32
+ this.options = options;
33
+ this.devServer = new DevServer({
34
+ projectDir: options.projectDir,
35
+ port: options.devPort,
36
+ env: options.env,
37
+ });
38
+ this.fileManager = new FileManager({ projectDir: options.projectDir });
39
+ this.checker = new Checker({ projectDir: options.projectDir, projectId: options.projectId });
40
+ this.errorStore = new ErrorStore();
41
+ this.packageInstaller = new PackageInstaller({ projectDir: options.projectDir });
42
+ }
43
+ getDevServer() {
44
+ return this.devServer;
45
+ }
46
+ getApiPort() {
47
+ return this.options.apiPort;
48
+ }
49
+ getDevPort() {
50
+ return this.options.devPort;
51
+ }
52
+ async start() {
53
+ this.server = http.createServer(async (req, res) => {
54
+ // CORS
55
+ res.setHeader('Access-Control-Allow-Origin', '*');
56
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
57
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
58
+ if (req.method === 'OPTIONS') {
59
+ res.statusCode = 204;
60
+ res.end();
61
+ return;
62
+ }
63
+ try {
64
+ await this.handleRequest(req, res);
65
+ }
66
+ catch (error) {
67
+ console.error(` [LocalServer] Error handling ${req.url}: ${error.message}`);
68
+ if (!res.headersSent) {
69
+ res.statusCode = 500;
70
+ res.setHeader('Content-Type', 'application/json');
71
+ res.end(JSON.stringify({ success: false, error: error.message }));
72
+ }
73
+ }
74
+ });
75
+ await new Promise((resolve, reject) => {
76
+ this.server.listen(this.options.apiPort, '127.0.0.1', () => {
77
+ console.log(` [LocalServer] API server on port ${this.options.apiPort}`);
78
+ resolve();
79
+ });
80
+ this.server.on('error', reject);
81
+ });
82
+ }
83
+ async stop() {
84
+ await this.devServer.stop();
85
+ if (this.server) {
86
+ this.server.close();
87
+ this.server = null;
88
+ }
89
+ }
90
+ async handleRequest(req, res) {
91
+ const url = new URL(req.url || '/', `http://localhost`);
92
+ const pathOnly = url.pathname;
93
+ // Route to handler
94
+ const handler = this.getHandler(pathOnly);
95
+ if (handler) {
96
+ const body = await this.collectBody(req);
97
+ await handler.call(this, req, res, body, url);
98
+ }
99
+ else {
100
+ res.statusCode = 404;
101
+ res.end(JSON.stringify({ error: `Unknown endpoint: ${pathOnly}` }));
102
+ }
103
+ }
104
+ getHandler(path) {
105
+ // Exact matches first
106
+ const handlers = {
107
+ '/live/sync': this.handleSync,
108
+ '/live/files': this.handleFiles,
109
+ '/live/check': this.handleCheck,
110
+ '/live/errors': this.handleErrors,
111
+ '/live/reload': this.handleReload,
112
+ '/live/runtime-error': this.handleRuntimeError,
113
+ '/live/install': this.handleInstall,
114
+ '/live/exec': this.handleExec,
115
+ '/live/terminal': this.handleTerminal,
116
+ '/live/container-status': this.handleContainerStatus,
117
+ '/live/container-stats': this.handleContainerStats,
118
+ '/live/logs': this.handleLogs,
119
+ '/live/dev': this.handleDev,
120
+ '/live/dev/restart': this.handleDevRestart,
121
+ '/live/dev/stop': this.handleDevStop,
122
+ '/live/open': this.handleOpen,
123
+ '/live/close': this.handleClose,
124
+ '/live/heartbeat': this.handleHeartbeat,
125
+ '/live/hard-patterns': this.handleHardPatterns,
126
+ '/live/hard-patterns-batch': this.handleHardPatternsBatch,
127
+ '/live/normalize': this.handleNormalize,
128
+ '/live/normalize-batch': this.handleNormalizeBatch,
129
+ '/live/invalidate': this.handleInvalidate,
130
+ '/live/refetch': this.handleRefetch,
131
+ '/live/grace-period': this.handleGracePeriod,
132
+ '/live/stats': this.handleStats,
133
+ '/live/usage': this.handleUsage,
134
+ '/health': this.handleHealth,
135
+ };
136
+ if (handlers[path])
137
+ return handlers[path];
138
+ // Prefix matches (e.g., /live/errors/123, /live/logs/123)
139
+ for (const [route, handler] of Object.entries(handlers)) {
140
+ if (path.startsWith(route + '/'))
141
+ return handler;
142
+ }
143
+ return null;
144
+ }
145
+ // ─── /live/sync ──────────────────────────────────────────────
146
+ async handleSync(_req, res, body) {
147
+ const data = JSON.parse(body);
148
+ const { files, deletedFiles, filterFiles } = data;
149
+ let filesWritten = 0;
150
+ let filesDeleted = 0;
151
+ // Write files
152
+ if (files && typeof files === 'object' && Object.keys(files).length > 0) {
153
+ const result = await this.fileManager.writeFiles(files);
154
+ filesWritten = result.filesWritten;
155
+ }
156
+ // Delete files
157
+ if (deletedFiles && Array.isArray(deletedFiles) && deletedFiles.length > 0) {
158
+ filesDeleted = await this.fileManager.deleteFiles(deletedFiles);
159
+ }
160
+ // Run type check
161
+ const filesToCheck = filterFiles || (files ? Object.keys(files) : []);
162
+ const { typeErrors, allErrors } = await this.checker.fullCheck(filesToCheck.length > 0 ? filesToCheck : undefined);
163
+ // Update error store
164
+ this.errorStore.setTypeErrors(typeErrors);
165
+ // Format response (matching gateway format)
166
+ const structuredErrors = allErrors;
167
+ const errorStrings = structuredErrors.map(structuredErrorToString);
168
+ this.json(res, {
169
+ success: true,
170
+ filesWritten,
171
+ filesDeleted,
172
+ structuredErrors,
173
+ errors: errorStrings,
174
+ errorCount: structuredErrors.length,
175
+ });
176
+ }
177
+ // ─── /live/files ─────────────────────────────────────────────
178
+ async handleFiles(_req, res, body) {
179
+ const data = JSON.parse(body);
180
+ const { files } = data;
181
+ if (!files) {
182
+ res.statusCode = 400;
183
+ this.json(res, { error: 'Missing files' });
184
+ return;
185
+ }
186
+ const result = await this.fileManager.writeFiles(files);
187
+ this.json(res, { success: true, filesWritten: result.filesWritten });
188
+ }
189
+ // ─── /live/check ─────────────────────────────────────────────
190
+ async handleCheck(_req, res, body) {
191
+ const data = body ? JSON.parse(body) : {};
192
+ const { filterFiles, autoRepair } = data;
193
+ const { typeErrors, allErrors } = await this.checker.fullCheck(filterFiles);
194
+ this.errorStore.setTypeErrors(typeErrors);
195
+ const structuredErrors = allErrors;
196
+ const errorStrings = structuredErrors.map(structuredErrorToString);
197
+ this.json(res, {
198
+ success: true,
199
+ structuredErrors,
200
+ errors: errorStrings,
201
+ errorCount: structuredErrors.length,
202
+ repairsApplied: false,
203
+ });
204
+ }
205
+ // ─── /live/errors ────────────────────────────────────────────
206
+ async handleErrors(_req, res, _body, url) {
207
+ // Try to extract projectId from URL path (e.g., /live/errors/123)
208
+ // Run a fresh check for accurate errors
209
+ const { typeErrors, allErrors } = await this.checker.fullCheck();
210
+ this.errorStore.setTypeErrors(typeErrors);
211
+ const all = this.errorStore.getAllErrors();
212
+ const byType = this.errorStore.getErrorsByType();
213
+ this.json(res, {
214
+ success: true,
215
+ errors: all.map(structuredErrorToString),
216
+ structuredErrors: all,
217
+ errorCount: all.length,
218
+ ...byType,
219
+ });
220
+ }
221
+ // ─── /live/reload ────────────────────────────────────────────
222
+ async handleReload(_req, res, body) {
223
+ // In local mode, Vite HMR handles reloading automatically.
224
+ // We just acknowledge the request.
225
+ this.json(res, { success: true, message: 'HMR handled by local dev server' });
226
+ }
227
+ // ─── /live/runtime-error ─────────────────────────────────────
228
+ async handleRuntimeError(_req, res, body) {
229
+ const data = JSON.parse(body);
230
+ const { error: errorMsg, file, line, col, stack } = data;
231
+ const message = errorMsg || 'Unknown runtime error';
232
+ const signature = `runtime:${file || 'unknown'}:${line || 0}:${message.substring(0, 100)}`;
233
+ this.errorStore.addRuntimeError({
234
+ file,
235
+ line,
236
+ col,
237
+ message,
238
+ type: 'runtime',
239
+ signature,
240
+ raw: stack || errorMsg,
241
+ });
242
+ this.json(res, { success: true });
243
+ }
244
+ // ─── /live/install ───────────────────────────────────────────
245
+ async handleInstall(_req, res, body) {
246
+ const data = JSON.parse(body);
247
+ const { packages, dev } = data;
248
+ if (!packages || !Array.isArray(packages) || packages.length === 0) {
249
+ res.statusCode = 400;
250
+ this.json(res, { error: 'Missing packages array' });
251
+ return;
252
+ }
253
+ const result = await this.packageInstaller.install(packages, dev);
254
+ this.json(res, {
255
+ success: result.success,
256
+ output: result.output,
257
+ installedPackages: result.installedPackages,
258
+ });
259
+ }
260
+ // ─── /live/exec ──────────────────────────────────────────────
261
+ async handleExec(_req, res, body) {
262
+ const data = JSON.parse(body);
263
+ const { command } = data;
264
+ if (!command) {
265
+ res.statusCode = 400;
266
+ this.json(res, { error: 'Missing command' });
267
+ return;
268
+ }
269
+ const result = await this.devServer.exec(command);
270
+ this.json(res, result);
271
+ }
272
+ // ─── /live/terminal ──────────────────────────────────────────
273
+ async handleTerminal(_req, res, body) {
274
+ const data = JSON.parse(body);
275
+ const { command, cwd } = data;
276
+ if (!command) {
277
+ res.statusCode = 400;
278
+ this.json(res, { error: 'Missing command' });
279
+ return;
280
+ }
281
+ const result = await this.devServer.exec(command);
282
+ this.json(res, {
283
+ success: result.exitCode === 0,
284
+ exitCode: result.exitCode,
285
+ stdout: result.stdout,
286
+ stderr: result.stderr,
287
+ });
288
+ }
289
+ // ─── /live/container-status ──────────────────────────────────
290
+ async handleContainerStatus(_req, res, _body, url) {
291
+ this.json(res, {
292
+ running: this.devServer.isRunning,
293
+ mode: 'agent',
294
+ agentMode: true,
295
+ hostname: os.hostname(),
296
+ platform: `${os.platform()} ${os.arch()}`,
297
+ });
298
+ }
299
+ // ─── /live/container-stats ───────────────────────────────────
300
+ async handleContainerStats(_req, res) {
301
+ const stats = this.devServer.getStats();
302
+ const totalMem = os.totalmem() / (1024 * 1024);
303
+ const freeMem = os.freemem() / (1024 * 1024);
304
+ this.json(res, {
305
+ success: true,
306
+ agentMode: true,
307
+ cpu: { percent: stats.cpuPercent, cores: os.cpus().length },
308
+ memory: {
309
+ usedMb: stats.memoryMb,
310
+ totalMb: Math.round(totalMem),
311
+ freeMb: Math.round(freeMem),
312
+ percent: totalMem > 0 ? Math.round((stats.memoryMb / totalMem) * 100) : 0,
313
+ },
314
+ pid: stats.pid,
315
+ uptime: this.devServer.uptime,
316
+ });
317
+ }
318
+ // ─── /live/logs ──────────────────────────────────────────────
319
+ async handleLogs(_req, res, _body, url) {
320
+ const limit = parseInt(url.searchParams.get('limit') || '100', 10);
321
+ const logs = this.devServer.getLogs(limit);
322
+ this.json(res, {
323
+ success: true,
324
+ logs: logs.map(l => ({
325
+ timestamp: new Date(l.timestamp).toISOString(),
326
+ type: l.type,
327
+ message: l.message,
328
+ source: 'frontend',
329
+ })),
330
+ });
331
+ }
332
+ // ─── /live/dev ───────────────────────────────────────────────
333
+ async handleDev(_req, res, body) {
334
+ try {
335
+ // Ensure dependencies are installed
336
+ await this.packageInstaller.ensureDependencies();
337
+ // Start dev server
338
+ await this.devServer.start();
339
+ this.json(res, {
340
+ success: true,
341
+ port: this.options.devPort,
342
+ mode: 'agent',
343
+ previewUrl: `http://localhost:${this.options.devPort}/`,
344
+ });
345
+ }
346
+ catch (err) {
347
+ res.statusCode = 500;
348
+ this.json(res, { success: false, error: err.message });
349
+ }
350
+ }
351
+ // ─── /live/dev/restart ───────────────────────────────────────
352
+ async handleDevRestart(_req, res) {
353
+ try {
354
+ await this.devServer.restart();
355
+ this.json(res, { success: true, message: 'Dev server restarted' });
356
+ }
357
+ catch (err) {
358
+ res.statusCode = 500;
359
+ this.json(res, { success: false, error: err.message });
360
+ }
361
+ }
362
+ // ─── /live/dev/stop ──────────────────────────────────────────
363
+ async handleDevStop(_req, res) {
364
+ await this.devServer.stop();
365
+ this.json(res, { success: true, message: 'Dev server stopped' });
366
+ }
367
+ // ─── /live/open ──────────────────────────────────────────────
368
+ async handleOpen(_req, res) {
369
+ try {
370
+ // Ensure deps are installed
371
+ await this.packageInstaller.ensureDependencies();
372
+ // Start dev server if not running
373
+ if (!this.devServer.isRunning) {
374
+ await this.devServer.start();
375
+ }
376
+ this.json(res, {
377
+ success: true,
378
+ port: this.options.devPort,
379
+ previewUrl: `http://localhost:${this.options.devPort}/`,
380
+ mode: 'agent',
381
+ agentMode: true,
382
+ });
383
+ }
384
+ catch (err) {
385
+ res.statusCode = 500;
386
+ this.json(res, { success: false, error: err.message });
387
+ }
388
+ }
389
+ // ─── /live/close ─────────────────────────────────────────────
390
+ async handleClose(_req, res) {
391
+ // Don't stop the dev server on close — user is still working locally.
392
+ // Just acknowledge.
393
+ this.json(res, { success: true, message: 'Acknowledged (agent keeps running)' });
394
+ }
395
+ // ─── /live/heartbeat ────────────────────────────────────────
396
+ async handleHeartbeat(_req, res) {
397
+ this.lastHeartbeat = Date.now();
398
+ this.json(res, {
399
+ success: true,
400
+ agentMode: true,
401
+ devServerRunning: this.devServer.isRunning,
402
+ });
403
+ }
404
+ // ─── /live/hard-patterns ─────────────────────────────────────
405
+ async handleHardPatterns(_req, res, body) {
406
+ const data = JSON.parse(body);
407
+ const { file, code } = data;
408
+ if (!file || !code) {
409
+ res.statusCode = 400;
410
+ this.json(res, { error: 'Missing file or code' });
411
+ return;
412
+ }
413
+ const errors = await this.checker.validateHardPatterns(file, code);
414
+ this.json(res, {
415
+ success: true,
416
+ errors: errors.map(structuredErrorToString),
417
+ structuredErrors: errors,
418
+ errorCount: errors.length,
419
+ passed: errors.length === 0,
420
+ });
421
+ }
422
+ // ─── /live/hard-patterns-batch ───────────────────────────────
423
+ async handleHardPatternsBatch(_req, res, body) {
424
+ const data = JSON.parse(body);
425
+ const { files } = data; // { "src/App.tsx": "code..." }
426
+ if (!files || typeof files !== 'object') {
427
+ res.statusCode = 400;
428
+ this.json(res, { error: 'Missing files object' });
429
+ return;
430
+ }
431
+ const allErrors = [];
432
+ for (const [filePath, code] of Object.entries(files)) {
433
+ const errors = await this.checker.validateHardPatterns(filePath, code);
434
+ allErrors.push(...errors);
435
+ }
436
+ this.json(res, {
437
+ success: true,
438
+ errors: allErrors.map(structuredErrorToString),
439
+ structuredErrors: allErrors,
440
+ errorCount: allErrors.length,
441
+ passed: allErrors.length === 0,
442
+ });
443
+ }
444
+ // ─── /live/normalize ─────────────────────────────────────────
445
+ async handleNormalize(_req, res, body) {
446
+ // In agent mode, normalization is a pass-through — the code is already local.
447
+ // We run hard pattern validation and type checking.
448
+ const data = JSON.parse(body);
449
+ const { file, code } = data;
450
+ if (!file || code === undefined) {
451
+ res.statusCode = 400;
452
+ this.json(res, { error: 'Missing file or code' });
453
+ return;
454
+ }
455
+ const hardErrors = await this.checker.validateHardPatterns(file, code);
456
+ this.json(res, {
457
+ success: true,
458
+ code, // Return code as-is (no normalization needed locally)
459
+ hardErrors: hardErrors.map(structuredErrorToString),
460
+ tsErrors: [],
461
+ repairsApplied: false,
462
+ });
463
+ }
464
+ // ─── /live/normalize-batch ───────────────────────────────────
465
+ async handleNormalizeBatch(_req, res, body) {
466
+ const data = JSON.parse(body);
467
+ const { files } = data;
468
+ if (!files || typeof files !== 'object') {
469
+ res.statusCode = 400;
470
+ this.json(res, { error: 'Missing files object' });
471
+ return;
472
+ }
473
+ const results = {};
474
+ for (const [filePath, code] of Object.entries(files)) {
475
+ const errors = await this.checker.validateHardPatterns(filePath, code);
476
+ results[filePath] = {
477
+ code: code,
478
+ hardErrors: errors.map(structuredErrorToString),
479
+ };
480
+ }
481
+ this.json(res, { success: true, results });
482
+ }
483
+ // ─── /live/invalidate ────────────────────────────────────────
484
+ async handleInvalidate(_req, res) {
485
+ // No cache to invalidate locally
486
+ this.json(res, { success: true, message: 'No-op in agent mode' });
487
+ }
488
+ // ─── /live/refetch ───────────────────────────────────────────
489
+ async handleRefetch(_req, res) {
490
+ // In agent mode, files are local — no need to refetch from DB
491
+ this.json(res, { success: true, message: 'Files are local, no refetch needed in agent mode' });
492
+ }
493
+ // ─── /live/grace-period ──────────────────────────────────────
494
+ async handleGracePeriod(_req, res) {
495
+ // No-op in agent mode
496
+ this.json(res, { success: true });
497
+ }
498
+ // ─── /live/stats ─────────────────────────────────────────────
499
+ async handleStats(_req, res) {
500
+ const stats = this.devServer.getStats();
501
+ this.json(res, {
502
+ success: true,
503
+ mode: 'agent',
504
+ agentMode: true,
505
+ hostname: os.hostname(),
506
+ platform: `${os.platform()} ${os.arch()}`,
507
+ devServerRunning: this.devServer.isRunning,
508
+ devServerPort: this.options.devPort,
509
+ uptime: this.devServer.uptime,
510
+ process: stats,
511
+ memory: {
512
+ totalMb: Math.round(os.totalmem() / (1024 * 1024)),
513
+ freeMb: Math.round(os.freemem() / (1024 * 1024)),
514
+ },
515
+ });
516
+ }
517
+ // ─── /live/usage ─────────────────────────────────────────────
518
+ async handleUsage(_req, res) {
519
+ // Agent mode: usage is free (local machine)
520
+ this.json(res, {
521
+ success: true,
522
+ agentMode: true,
523
+ usage: {
524
+ projectId: this.options.projectId,
525
+ mode: 'agent',
526
+ seconds: Math.round(this.devServer.uptime / 1000),
527
+ requests: 0,
528
+ },
529
+ });
530
+ }
531
+ // ─── /health ─────────────────────────────────────────────────
532
+ async handleHealth(_req, res) {
533
+ this.json(res, {
534
+ status: 'ok',
535
+ mode: 'agent',
536
+ agentMode: true,
537
+ devServer: this.devServer.isRunning,
538
+ uptime: this.devServer.uptime,
539
+ });
540
+ }
541
+ // ─── Helpers ─────────────────────────────────────────────────
542
+ json(res, data) {
543
+ res.setHeader('Content-Type', 'application/json');
544
+ res.setHeader('Access-Control-Allow-Origin', '*');
545
+ res.end(JSON.stringify(data));
546
+ }
547
+ collectBody(req) {
548
+ return new Promise((resolve) => {
549
+ let body = '';
550
+ req.on('data', (chunk) => { body += chunk.toString(); });
551
+ req.on('end', () => resolve(body));
552
+ });
553
+ }
554
+ }
555
+ //# sourceMappingURL=localServer.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Package Installer — handles npm/pnpm package installation locally.
3
+ * Replaces the container-based /live/install endpoint.
4
+ */
5
+ export interface PackageInstallerOptions {
6
+ projectDir: string;
7
+ }
8
+ export declare class PackageInstaller {
9
+ private projectDir;
10
+ constructor(options: PackageInstallerOptions);
11
+ /**
12
+ * Install packages into the project.
13
+ */
14
+ install(packages: string[], dev?: boolean): Promise<{
15
+ success: boolean;
16
+ output: string;
17
+ installedPackages: string[];
18
+ }>;
19
+ /**
20
+ * Ensure all project dependencies are installed.
21
+ */
22
+ ensureDependencies(): Promise<void>;
23
+ /**
24
+ * Detect which package manager is used (pnpm, yarn, npm).
25
+ */
26
+ private detectPackageManager;
27
+ private buildInstallArgs;
28
+ private runCommand;
29
+ }