nstantpage-agent 0.5.27 → 0.5.29

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.
@@ -81,5 +81,15 @@ export declare class AgentSync {
81
81
  private snapshotBaseline;
82
82
  /** Read a single file from disk (for diff view) */
83
83
  readDiskFile(filePath: string): Promise<string | null>;
84
+ /**
85
+ * Pull files from the backend DB and write them to disk.
86
+ * This fetches ALL project files and overwrites local versions.
87
+ * Files that exist only on disk (not in DB) are left untouched.
88
+ */
89
+ pullFromBackend(): Promise<{
90
+ success: boolean;
91
+ filesWritten: number;
92
+ versionId?: string;
93
+ }>;
84
94
  destroy(): void;
85
95
  }
package/dist/agentSync.js CHANGED
@@ -278,6 +278,13 @@ export class AgentSync {
278
278
  continue;
279
279
  if (SKIP_EXTENSIONS.has(ext))
280
280
  continue;
281
+ // Match disk scan: skip files inside SKIP_DIRS directories
282
+ const parts = filePath.split('/');
283
+ if (parts.some(p => SKIP_DIRS.has(p)))
284
+ continue;
285
+ // Match disk scan: skip dist/ and build/ top-level paths
286
+ if (filePath.startsWith('dist/') || filePath.startsWith('build/'))
287
+ continue;
281
288
  filtered.set(filePath, sha);
282
289
  }
283
290
  return filtered;
@@ -358,36 +365,11 @@ export class AgentSync {
358
365
  }
359
366
  }
360
367
  // Determine direction
368
+ // Disk is always the source of truth. DB only changes via push or AI coding agent
369
+ // (which updates both disk+DB simultaneously). So direction is only ever
370
+ // 'in-sync' or 'disk-ahead'. backendOnlyFiles are stale DB remnants, not "newer" files.
361
371
  const inSync = modifiedFiles.length === 0 && diskOnlyFiles.length === 0 && backendOnlyFiles.length === 0;
362
- let direction = 'in-sync';
363
- if (!inSync) {
364
- const hasDiskEdits = modifiedFiles.length > 0 || diskOnlyFiles.length > 0;
365
- const hasBackendEdits = backendOnlyFiles.length > 0;
366
- let modifiedFromBackend = false;
367
- if (modifiedFiles.length > 0 && baseline) {
368
- for (const filePath of modifiedFiles) {
369
- const baselineSha = baseline.get(filePath);
370
- const diskSha = diskResult.checksums.get(filePath);
371
- const backendSha = filteredBackend.get(filePath);
372
- if (baselineSha && diskSha === baselineSha && backendSha && backendSha !== baselineSha) {
373
- modifiedFromBackend = true;
374
- break;
375
- }
376
- }
377
- }
378
- if ((hasDiskEdits && !modifiedFromBackend) && hasBackendEdits) {
379
- direction = 'diverged';
380
- }
381
- else if (hasDiskEdits && !modifiedFromBackend && !hasBackendEdits) {
382
- direction = 'disk-ahead';
383
- }
384
- else if (hasBackendEdits || modifiedFromBackend) {
385
- direction = 'backend-ahead';
386
- }
387
- else {
388
- direction = 'disk-ahead';
389
- }
390
- }
372
+ const direction = inSync ? 'in-sync' : 'disk-ahead';
391
373
  return {
392
374
  inSync,
393
375
  direction,
@@ -410,12 +392,14 @@ export class AgentSync {
410
392
  const backendResult = await this.fetchBackendChecksums();
411
393
  if (!backendResult)
412
394
  return [];
395
+ // Use filtered backend checksums (same skip rules as disk scan) for consistency with getSyncStatus
396
+ const filteredBackend = this.filterBackendChecksums(backendResult.checksums);
413
397
  const baseline = this.syncedBaselineChecksums;
414
398
  const result = [];
415
399
  if (baseline) {
416
400
  for (const [filePath, baselineSha] of baseline) {
417
401
  const currentDiskSha = diskComputed.checksums.get(filePath);
418
- const backendSha = backendResult.checksums.get(filePath);
402
+ const backendSha = filteredBackend.get(filePath);
419
403
  if (!currentDiskSha) {
420
404
  if (backendSha) {
421
405
  result.push({ path: filePath, status: 'modified', backendSha });
@@ -431,7 +415,7 @@ export class AgentSync {
431
415
  }
432
416
  }
433
417
  if (this.lastSyncTimestamp && this.lastSyncTimestamp.versionId !== backendResult.versionId) {
434
- for (const [filePath, backendSha] of backendResult.checksums) {
418
+ for (const [filePath, backendSha] of filteredBackend) {
435
419
  if (!baseline.has(filePath) && !diskComputed.checksums.has(filePath)) {
436
420
  result.push({ path: filePath, status: 'backend-only', backendSha });
437
421
  }
@@ -439,7 +423,7 @@ export class AgentSync {
439
423
  }
440
424
  }
441
425
  else {
442
- const sortedBackend = [...backendResult.checksums.entries()].sort((a, b) => a[0].localeCompare(b[0]));
426
+ const sortedBackend = [...filteredBackend.entries()].sort((a, b) => a[0].localeCompare(b[0]));
443
427
  for (const [filePath, backendSha] of sortedBackend) {
444
428
  const diskSha = diskComputed.checksums.get(filePath);
445
429
  if (diskSha) {
@@ -456,7 +440,7 @@ export class AgentSync {
456
440
  }
457
441
  const sortedDisk = [...diskComputed.checksums.entries()].sort((a, b) => a[0].localeCompare(b[0]));
458
442
  for (const [filePath, diskSha] of sortedDisk) {
459
- if (!backendResult.checksums.has(filePath) && isUserCreatedFile(filePath)) {
443
+ if (!filteredBackend.has(filePath) && isUserCreatedFile(filePath)) {
460
444
  result.push({ path: filePath, status: 'disk-only', diskSha });
461
445
  }
462
446
  }
@@ -578,6 +562,49 @@ export class AgentSync {
578
562
  return null;
579
563
  }
580
564
  }
565
+ /**
566
+ * Pull files from the backend DB and write them to disk.
567
+ * This fetches ALL project files and overwrites local versions.
568
+ * Files that exist only on disk (not in DB) are left untouched.
569
+ */
570
+ async pullFromBackend() {
571
+ const url = `${this.backendUrl}/api/sandbox/files?projectId=${this.projectId}`;
572
+ const response = await fetch(url);
573
+ if (!response.ok) {
574
+ throw new Error(`Failed to fetch files from backend: ${response.status}`);
575
+ }
576
+ const data = await response.json();
577
+ if (!data.files || data.files.length === 0) {
578
+ return { success: true, filesWritten: 0 };
579
+ }
580
+ // Ensure directories exist
581
+ const dirsToCreate = new Set();
582
+ for (const file of data.files) {
583
+ const filePath = path.join(this.projectDir, file.path);
584
+ dirsToCreate.add(path.dirname(filePath));
585
+ }
586
+ for (const dir of dirsToCreate) {
587
+ await fs.mkdir(dir, { recursive: true }).catch(() => { });
588
+ }
589
+ // Write files in parallel batches
590
+ const BATCH_SIZE = 50;
591
+ let written = 0;
592
+ for (let i = 0; i < data.files.length; i += BATCH_SIZE) {
593
+ const batch = data.files.slice(i, i + BATCH_SIZE);
594
+ await Promise.all(batch.map(async (file) => {
595
+ const filePath = path.join(this.projectDir, file.path);
596
+ await fs.writeFile(filePath, file.content, 'utf-8');
597
+ }));
598
+ written += batch.length;
599
+ }
600
+ // Invalidate caches and re-establish baseline
601
+ this.diskChecksumCache = null;
602
+ this.backendChecksumCache = null;
603
+ const versionId = String(data.versionId);
604
+ this.markSynced(versionId);
605
+ console.log(` [AgentSync] Pulled ${written} files from backend (version ${versionId})`);
606
+ return { success: true, filesWritten: written, versionId };
607
+ }
581
608
  // ─── Cleanup ──────────────────────────────────────────────
582
609
  destroy() {
583
610
  this.stopFileWatcher();
package/dist/cli.js CHANGED
@@ -25,7 +25,7 @@ const program = new Command();
25
25
  program
26
26
  .name('nstantpage')
27
27
  .description('Local development agent for nstantpage.com — run projects on your machine, preview in the cloud')
28
- .version('0.5.27');
28
+ .version('0.5.29');
29
29
  program
30
30
  .command('login')
31
31
  .description('Authenticate with nstantpage.com')
@@ -26,7 +26,7 @@ import { LocalServer } from '../localServer.js';
26
26
  import { PackageInstaller } from '../packageInstaller.js';
27
27
  import { probeLocalPostgres, ensureLocalProjectDb, closeAdminPool, writeDatabaseUrlToEnv } from '../projectDb.js';
28
28
  import { StatusServer } from '../statusServer.js';
29
- const VERSION = '0.5.27';
29
+ const VERSION = '0.5.29';
30
30
  /**
31
31
  * Resolve the backend API base URL.
32
32
  * - If --backend is passed, use it
@@ -127,6 +127,7 @@ export declare class LocalServer {
127
127
  private handleSyncStatus;
128
128
  private handleSyncDiff;
129
129
  private handlePushToBackend;
130
+ private handlePullFromBackend;
130
131
  private handleDiskFile;
131
132
  private handleHealth;
132
133
  /**
@@ -282,6 +282,7 @@ export class LocalServer {
282
282
  '/live/sync-status': this.handleSyncStatus,
283
283
  '/live/sync-diff': this.handleSyncDiff,
284
284
  '/live/push-to-backend': this.handlePushToBackend,
285
+ '/live/pull-from-backend': this.handlePullFromBackend,
285
286
  '/live/disk-file': this.handleDiskFile,
286
287
  '/health': this.handleHealth,
287
288
  };
@@ -1102,6 +1103,21 @@ export class LocalServer {
1102
1103
  this.json(res, { success: false, error: error.message }, 500);
1103
1104
  }
1104
1105
  }
1106
+ // ─── /live/pull-from-backend ──────────────────────────────────
1107
+ async handlePullFromBackend(_req, res) {
1108
+ if (!this.agentSync) {
1109
+ this.json(res, { success: false, error: 'Sync not available (no backendUrl configured)' }, 503);
1110
+ return;
1111
+ }
1112
+ try {
1113
+ const result = await this.agentSync.pullFromBackend();
1114
+ this.json(res, result);
1115
+ }
1116
+ catch (error) {
1117
+ console.error(` [LocalServer] pull-from-backend error:`, error.message);
1118
+ this.json(res, { success: false, error: error.message }, 500);
1119
+ }
1120
+ }
1105
1121
  // ─── /live/disk-file ─────────────────────────────────────────
1106
1122
  async handleDiskFile(_req, res, _body, url) {
1107
1123
  const filePath = url.searchParams.get('path');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nstantpage-agent",
3
- "version": "0.5.27",
3
+ "version": "0.5.29",
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": {