nstantpage-agent 0.8.8 → 0.8.10

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.
@@ -1,80 +1,26 @@
1
1
  /**
2
- * agentSync.ts — Simplified file sync for the local agent.
2
+ * agentSync.ts — File watcher for HMR notifications.
3
3
  *
4
- * CORE PRINCIPLE: Disk is ALWAYS the source of truth. ONE-WAY only.
5
- *
6
- * When files change on disk do a FULL SYNC (same as `nstantpage sync`):
7
- * 1. POST /api/projects/{id}/sync — new version + clear ALL FullFiles
8
- * 2. Collect ALL disk files
9
- * 3. POST /api/sandbox/push-files — push ALL files in batches
10
- *
11
- * No checksums, no comparisons, no diffs. Disk wins. Always.
4
+ * Watches disk for file changes and fires onSyncDirty callback
5
+ * so the gateway can trigger HMR reload in the browser preview.
6
+ * That's it. No sync, no push, no pull, no status, no diffs.
12
7
  */
13
- export type SyncDirection = 'in-sync' | 'backend-ahead' | 'disk-ahead' | 'diverged';
14
- export interface SyncStatusResult {
15
- inSync: boolean;
16
- direction: SyncDirection;
17
- diskChecksum: string | null;
18
- backendChecksum: string | null;
19
- diskFileCount: number;
20
- backendFileCount: number;
21
- modifiedFiles: string[];
22
- diskOnlyFiles: string[];
23
- backendOnlyFiles: string[];
24
- lastSyncedVersionId: string | null;
25
- lastSyncedAt: number | null;
26
- diskDirty: boolean;
27
- computedAt: number;
28
- }
29
- export interface PerFileStatus {
30
- path: string;
31
- status: 'same' | 'modified' | 'disk-only' | 'backend-only';
32
- diskSha?: string;
33
- backendSha?: string;
34
- }
35
8
  export interface AgentSyncOptions {
36
9
  projectDir: string;
37
10
  projectId: string;
38
11
  backendUrl: string;
39
- /** Called when file watcher detects changes — should send sync-dirty via tunnel */
12
+ /** Called when file watcher detects changes — triggers HMR via tunnel */
40
13
  onSyncDirty?: (projectId: string) => void;
41
14
  }
42
15
  export declare class AgentSync {
43
16
  private projectDir;
44
17
  private projectId;
45
- private backendUrl;
46
18
  private onSyncDirty?;
47
19
  private fileWatcher;
48
20
  private debounceTimer;
49
- private syncing;
50
- private lastVersionId;
51
- private lastSyncAt;
52
21
  constructor(options: AgentSyncOptions);
53
22
  startFileWatcher(): void;
54
23
  stopFileWatcher(): void;
55
- getSyncStatus(): Promise<SyncStatusResult>;
56
- getPerFileStatus(): Promise<PerFileStatus[]>;
57
- pushToBackend(): Promise<{
58
- success: boolean;
59
- filesPushed: number;
60
- filesDeleted: number;
61
- modifiedFiles: string[];
62
- addedFiles: string[];
63
- deletedFiles: string[];
64
- versionId?: string;
65
- message?: string;
66
- }>;
67
- markSynced(versionId: string): void;
68
- /** Read a single file from disk (for diff view) */
69
- readDiskFile(filePath: string): Promise<string | null>;
70
- /**
71
- * Pull files from the backend DB and write them to disk.
72
- * After writing, clears pending changes (the writes would trigger the watcher).
73
- */
74
- pullFromBackend(): Promise<{
75
- success: boolean;
76
- filesWritten: number;
77
- versionId?: string;
78
- }>;
24
+ markSynced(_versionId: string): void;
79
25
  destroy(): void;
80
26
  }
package/dist/agentSync.js CHANGED
@@ -1,17 +1,11 @@
1
1
  /**
2
- * agentSync.ts — Simplified file sync for the local agent.
2
+ * agentSync.ts — File watcher for HMR notifications.
3
3
  *
4
- * CORE PRINCIPLE: Disk is ALWAYS the source of truth. ONE-WAY only.
5
- *
6
- * When files change on disk do a FULL SYNC (same as `nstantpage sync`):
7
- * 1. POST /api/projects/{id}/sync — new version + clear ALL FullFiles
8
- * 2. Collect ALL disk files
9
- * 3. POST /api/sandbox/push-files — push ALL files in batches
10
- *
11
- * No checksums, no comparisons, no diffs. Disk wins. Always.
4
+ * Watches disk for file changes and fires onSyncDirty callback
5
+ * so the gateway can trigger HMR reload in the browser preview.
6
+ * That's it. No sync, no push, no pull, no status, no diffs.
12
7
  */
13
8
  import path from 'path';
14
- import fs from 'fs/promises';
15
9
  import { watch, existsSync } from 'fs';
16
10
  // ─── Skip Patterns ────────────────────────────────────────────
17
11
  const SKIP_DIRS = new Set([
@@ -33,8 +27,6 @@ const SKIP_EXTENSIONS = new Set([
33
27
  '.sqlite', '.db',
34
28
  '.svg',
35
29
  ]);
36
- /** Max file size to sync (1MB) */
37
- const MAX_FILE_SIZE = 1_048_576;
38
30
  function shouldSkip(name, isDir) {
39
31
  if (isDir)
40
32
  return SKIP_DIRS.has(name) || name.startsWith('.');
@@ -45,7 +37,6 @@ function shouldSkip(name, isDir) {
45
37
  const ext = path.extname(name).toLowerCase();
46
38
  return SKIP_EXTENSIONS.has(ext);
47
39
  }
48
- /** Check if a relative path should be skipped (checks every path component) */
49
40
  function shouldSkipPath(relPath) {
50
41
  const parts = relPath.split(/[\/\\]/);
51
42
  for (let i = 0; i < parts.length - 1; i++) {
@@ -54,60 +45,17 @@ function shouldSkipPath(relPath) {
54
45
  }
55
46
  return shouldSkip(parts[parts.length - 1], false);
56
47
  }
57
- // ─── Collect All Files (same as sync.ts) ──────────────────────
58
- async function collectAllFiles(baseDir, currentDir) {
59
- const files = [];
60
- let entries;
61
- try {
62
- entries = await fs.readdir(currentDir, { withFileTypes: true });
63
- }
64
- catch {
65
- return files;
66
- }
67
- for (const entry of entries) {
68
- const fullPath = path.join(currentDir, entry.name);
69
- if (entry.isDirectory()) {
70
- if (shouldSkip(entry.name, true))
71
- continue;
72
- const sub = await collectAllFiles(baseDir, fullPath);
73
- files.push(...sub);
74
- }
75
- else if (entry.isFile()) {
76
- if (shouldSkip(entry.name, false))
77
- continue;
78
- try {
79
- const stat = await fs.stat(fullPath);
80
- if (stat.size > MAX_FILE_SIZE)
81
- continue;
82
- const content = await fs.readFile(fullPath, 'utf-8');
83
- if (content.includes('\0'))
84
- continue; // binary
85
- const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
86
- files.push({ relativePath, content });
87
- }
88
- catch { /* unreadable — skip */ }
89
- }
90
- }
91
- return files;
92
- }
93
48
  export class AgentSync {
94
49
  projectDir;
95
50
  projectId;
96
- backendUrl;
97
51
  onSyncDirty;
98
- // State
99
52
  fileWatcher = null;
100
53
  debounceTimer = null;
101
- syncing = false;
102
- lastVersionId = null;
103
- lastSyncAt = null;
104
54
  constructor(options) {
105
55
  this.projectDir = options.projectDir;
106
56
  this.projectId = options.projectId;
107
- this.backendUrl = options.backendUrl;
108
57
  this.onSyncDirty = options.onSyncDirty;
109
58
  }
110
- // ─── File Watcher ─────────────────────────────────────────
111
59
  startFileWatcher() {
112
60
  if (this.fileWatcher)
113
61
  return;
@@ -119,7 +67,6 @@ export class AgentSync {
119
67
  return;
120
68
  if (shouldSkipPath(filename))
121
69
  return;
122
- // Debounce — just fire HMR notification, no file tracking
123
70
  if (this.debounceTimer)
124
71
  clearTimeout(this.debounceTimer);
125
72
  this.debounceTimer = setTimeout(() => {
@@ -149,160 +96,8 @@ export class AgentSync {
149
96
  this.debounceTimer = null;
150
97
  }
151
98
  }
152
- // ─── Sync Status ─────────────────────────────────────────
153
- async getSyncStatus() {
154
- return {
155
- inSync: true,
156
- direction: 'in-sync',
157
- diskChecksum: null,
158
- backendChecksum: null,
159
- diskFileCount: 0,
160
- backendFileCount: 0,
161
- modifiedFiles: [],
162
- diskOnlyFiles: [],
163
- backendOnlyFiles: [],
164
- lastSyncedVersionId: this.lastVersionId,
165
- lastSyncedAt: this.lastSyncAt,
166
- diskDirty: false,
167
- computedAt: Date.now(),
168
- };
169
- }
170
- // ─── Per-File Status ──────────────────────────────────────
171
- async getPerFileStatus() {
172
- return [];
173
- }
174
- // ─── Push to Backend (Full Sync) ──────────────────────────
175
- //
176
- // Does EXACTLY what `nstantpage sync` does:
177
- // 1. POST /api/projects/{id}/sync — new version + clear ALL FullFiles
178
- // 2. Collect ALL files from disk
179
- // 3. POST /api/sandbox/push-files — push ALL files in batches of 100
180
- async pushToBackend() {
181
- if (this.syncing) {
182
- return {
183
- success: true, filesPushed: 0, filesDeleted: 0,
184
- modifiedFiles: [], addedFiles: [], deletedFiles: [],
185
- message: 'Sync already in progress',
186
- };
187
- }
188
- this.syncing = true;
189
- try {
190
- // Step 1: Create new version + clear all FullFiles
191
- const syncRes = await fetch(`${this.backendUrl}/api/projects/${this.projectId}/sync`, {
192
- method: 'POST',
193
- headers: { 'Content-Type': 'application/json' },
194
- });
195
- if (!syncRes.ok) {
196
- const text = await syncRes.text().catch(() => '');
197
- throw new Error(`Sync init failed (${syncRes.status}): ${text}`);
198
- }
199
- const syncData = await syncRes.json();
200
- console.log(` [AgentSync] New version ${syncData.versionNumber} created (old files cleared)`);
201
- // Step 2: Collect ALL files from disk
202
- const allFiles = await collectAllFiles(this.projectDir, this.projectDir);
203
- // Step 3: Push in batches of 100
204
- const BATCH_SIZE = 100;
205
- let totalPushed = 0;
206
- for (let i = 0; i < allFiles.length; i += BATCH_SIZE) {
207
- const batch = allFiles.slice(i, i + BATCH_SIZE);
208
- const filesMap = {};
209
- for (const f of batch) {
210
- filesMap[f.relativePath] = f.content;
211
- }
212
- const pushRes = await fetch(`${this.backendUrl}/api/sandbox/push-files`, {
213
- method: 'POST',
214
- headers: { 'Content-Type': 'application/json' },
215
- body: JSON.stringify({
216
- projectId: parseInt(this.projectId, 10),
217
- files: filesMap,
218
- }),
219
- });
220
- if (!pushRes.ok) {
221
- const text = await pushRes.text().catch(() => '');
222
- throw new Error(`Push batch failed (${pushRes.status}): ${text}`);
223
- }
224
- totalPushed += batch.length;
225
- }
226
- // Done
227
- this.lastVersionId = String(syncData.versionId);
228
- this.lastSyncAt = Date.now();
229
- const fileNames = allFiles.map(f => f.relativePath);
230
- console.log(` [AgentSync] Full sync complete: ${totalPushed} files pushed (version ${syncData.versionNumber})`);
231
- return {
232
- success: true,
233
- filesPushed: totalPushed,
234
- filesDeleted: 0,
235
- modifiedFiles: fileNames,
236
- addedFiles: [],
237
- deletedFiles: [],
238
- versionId: String(syncData.versionId),
239
- };
240
- }
241
- catch (err) {
242
- console.error(` [AgentSync] Sync failed:`, err.message);
243
- throw err;
244
- }
245
- finally {
246
- this.syncing = false;
247
- }
248
- }
249
- // ─── Sync Markers (for compatibility with start.ts) ───────
250
- markSynced(versionId) {
251
- this.lastVersionId = versionId;
252
- this.lastSyncAt = Date.now();
253
- console.log(` [AgentSync] Marked synced at version ${versionId}`);
254
- }
255
- /** Read a single file from disk (for diff view) */
256
- async readDiskFile(filePath) {
257
- try {
258
- const normalized = path.normalize(filePath).replace(/\.\.\//g, '');
259
- const fullPath = path.join(this.projectDir, normalized);
260
- if (!fullPath.startsWith(this.projectDir))
261
- return null;
262
- return await fs.readFile(fullPath, 'utf-8');
263
- }
264
- catch {
265
- return null;
266
- }
267
- }
268
- /**
269
- * Pull files from the backend DB and write them to disk.
270
- * After writing, clears pending changes (the writes would trigger the watcher).
271
- */
272
- async pullFromBackend() {
273
- const url = `${this.backendUrl}/api/sandbox/files?projectId=${this.projectId}`;
274
- const response = await fetch(url);
275
- if (!response.ok) {
276
- throw new Error(`Failed to fetch files from backend: ${response.status}`);
277
- }
278
- const data = await response.json();
279
- if (!data.files || data.files.length === 0) {
280
- return { success: true, filesWritten: 0 };
281
- }
282
- const dirsToCreate = new Set();
283
- for (const file of data.files) {
284
- const fp = path.join(this.projectDir, file.path);
285
- dirsToCreate.add(path.dirname(fp));
286
- }
287
- for (const dir of dirsToCreate) {
288
- await fs.mkdir(dir, { recursive: true }).catch(() => { });
289
- }
290
- const BATCH_SIZE = 50;
291
- let written = 0;
292
- for (let i = 0; i < data.files.length; i += BATCH_SIZE) {
293
- const batch = data.files.slice(i, i + BATCH_SIZE);
294
- await Promise.all(batch.map(async (file) => {
295
- const fp = path.join(this.projectDir, file.path);
296
- await fs.writeFile(fp, file.content, 'utf-8');
297
- }));
298
- written += batch.length;
299
- }
300
- this.lastVersionId = String(data.versionId);
301
- this.lastSyncAt = Date.now();
302
- console.log(` [AgentSync] Pulled ${written} files from backend (version ${data.versionId})`);
303
- return { success: true, filesWritten: written, versionId: String(data.versionId) };
304
- }
305
- // ─── Cleanup ──────────────────────────────────────────────
99
+ // Stubs kept for API compat so localServer.ts doesn't break
100
+ markSynced(_versionId) { }
306
101
  destroy() {
307
102
  this.stopFileWatcher();
308
103
  }
@@ -124,11 +124,6 @@ export declare class LocalServer {
124
124
  private handleStats;
125
125
  private handleUsage;
126
126
  private handleBuild;
127
- private handleSyncStatus;
128
- private handleSyncDiff;
129
- private handlePushToBackend;
130
- private handlePullFromBackend;
131
- private handleDiskFile;
132
127
  private handleTree;
133
128
  private handleFileContent;
134
129
  private handleSaveFile;
@@ -401,11 +401,6 @@ export class LocalServer {
401
401
  '/live/db/query': this.handleDbQuery,
402
402
  '/live/db/create': this.handleDbCreate,
403
403
  '/live/build': this.handleBuild,
404
- '/live/sync-status': this.handleSyncStatus,
405
- '/live/sync-diff': this.handleSyncDiff,
406
- '/live/push-to-backend': this.handlePushToBackend,
407
- '/live/pull-from-backend': this.handlePullFromBackend,
408
- '/live/disk-file': this.handleDiskFile,
409
404
  '/live/tree': this.handleTree,
410
405
  '/live/file-content': this.handleFileContent,
411
406
  '/live/save-file': this.handleSaveFile,
@@ -1186,98 +1181,6 @@ export class LocalServer {
1186
1181
  this.json(res, { success: false, error: err.message });
1187
1182
  }
1188
1183
  }
1189
- // ─── /live/sync-status ────────────────────────────────────────
1190
- async handleSyncStatus(_req, res, _body, url) {
1191
- if (!this.agentSync) {
1192
- this.json(res, { success: false, error: 'Sync not available (no backendUrl configured)' }, 503);
1193
- return;
1194
- }
1195
- try {
1196
- const status = await this.agentSync.getSyncStatus();
1197
- this.json(res, { success: true, ...status });
1198
- }
1199
- catch (error) {
1200
- console.error(` [LocalServer] sync-status error:`, error.message);
1201
- this.json(res, { success: false, error: error.message }, 500);
1202
- }
1203
- }
1204
- // ─── /live/sync-diff ─────────────────────────────────────────
1205
- async handleSyncDiff(_req, res, _body, url) {
1206
- if (!this.agentSync) {
1207
- this.json(res, { success: false, error: 'Sync not available (no backendUrl configured)' }, 503);
1208
- return;
1209
- }
1210
- try {
1211
- const files = await this.agentSync.getPerFileStatus();
1212
- const changed = files.filter(f => f.status !== 'same');
1213
- this.json(res, {
1214
- success: true,
1215
- totalFiles: files.length,
1216
- changedCount: changed.length,
1217
- files: changed,
1218
- });
1219
- }
1220
- catch (error) {
1221
- console.error(` [LocalServer] sync-diff error:`, error.message);
1222
- this.json(res, { success: false, error: error.message }, 500);
1223
- }
1224
- }
1225
- // ─── /live/push-to-backend ───────────────────────────────────
1226
- async handlePushToBackend(_req, res, _body) {
1227
- if (!this.agentSync) {
1228
- this.json(res, { success: false, error: 'Sync not available (no backendUrl configured)' }, 503);
1229
- return;
1230
- }
1231
- try {
1232
- const result = await this.agentSync.pushToBackend();
1233
- this.json(res, result);
1234
- }
1235
- catch (error) {
1236
- console.error(` [LocalServer] push-to-backend error:`, error.message);
1237
- this.json(res, { success: false, error: error.message }, 500);
1238
- }
1239
- }
1240
- // ─── /live/pull-from-backend ──────────────────────────────────
1241
- async handlePullFromBackend(_req, res) {
1242
- if (!this.agentSync) {
1243
- this.json(res, { success: false, error: 'Sync not available (no backendUrl configured)' }, 503);
1244
- return;
1245
- }
1246
- try {
1247
- const result = await this.agentSync.pullFromBackend();
1248
- this.json(res, result);
1249
- }
1250
- catch (error) {
1251
- console.error(` [LocalServer] pull-from-backend error:`, error.message);
1252
- this.json(res, { success: false, error: error.message }, 500);
1253
- }
1254
- }
1255
- // ─── /live/disk-file ─────────────────────────────────────────
1256
- async handleDiskFile(_req, res, _body, url) {
1257
- const filePath = url.searchParams.get('path');
1258
- if (!filePath) {
1259
- this.json(res, { error: 'Missing path parameter' }, 400);
1260
- return;
1261
- }
1262
- if (!this.agentSync) {
1263
- this.json(res, { success: false, error: 'Sync not available' }, 503);
1264
- return;
1265
- }
1266
- try {
1267
- const content = await this.agentSync.readDiskFile(filePath);
1268
- if (content === null) {
1269
- this.json(res, { error: 'File not found' }, 404);
1270
- return;
1271
- }
1272
- res.statusCode = 200;
1273
- res.setHeader('Content-Type', 'text/plain; charset=utf-8');
1274
- res.setHeader('Access-Control-Allow-Origin', '*');
1275
- res.end(content);
1276
- }
1277
- catch (error) {
1278
- this.json(res, { success: false, error: error.message }, 500);
1279
- }
1280
- }
1281
1184
  // ─── /live/tree ──────────────────────────────────────────────
1282
1185
  async handleTree(_req, res) {
1283
1186
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nstantpage-agent",
3
- "version": "0.8.8",
3
+ "version": "0.8.10",
4
4
  "description": "Local development agent for nstantpage.com — run your projects locally, preview in the cloud. Replaces cloud containers for faster builds.",
5
5
  "type": "module",
6
6
  "bin": {