nstantpage-agent 0.5.27 → 0.5.28

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
@@ -410,12 +410,14 @@ export class AgentSync {
410
410
  const backendResult = await this.fetchBackendChecksums();
411
411
  if (!backendResult)
412
412
  return [];
413
+ // Use filtered backend checksums (same skip rules as disk scan) for consistency with getSyncStatus
414
+ const filteredBackend = this.filterBackendChecksums(backendResult.checksums);
413
415
  const baseline = this.syncedBaselineChecksums;
414
416
  const result = [];
415
417
  if (baseline) {
416
418
  for (const [filePath, baselineSha] of baseline) {
417
419
  const currentDiskSha = diskComputed.checksums.get(filePath);
418
- const backendSha = backendResult.checksums.get(filePath);
420
+ const backendSha = filteredBackend.get(filePath);
419
421
  if (!currentDiskSha) {
420
422
  if (backendSha) {
421
423
  result.push({ path: filePath, status: 'modified', backendSha });
@@ -431,7 +433,7 @@ export class AgentSync {
431
433
  }
432
434
  }
433
435
  if (this.lastSyncTimestamp && this.lastSyncTimestamp.versionId !== backendResult.versionId) {
434
- for (const [filePath, backendSha] of backendResult.checksums) {
436
+ for (const [filePath, backendSha] of filteredBackend) {
435
437
  if (!baseline.has(filePath) && !diskComputed.checksums.has(filePath)) {
436
438
  result.push({ path: filePath, status: 'backend-only', backendSha });
437
439
  }
@@ -439,7 +441,7 @@ export class AgentSync {
439
441
  }
440
442
  }
441
443
  else {
442
- const sortedBackend = [...backendResult.checksums.entries()].sort((a, b) => a[0].localeCompare(b[0]));
444
+ const sortedBackend = [...filteredBackend.entries()].sort((a, b) => a[0].localeCompare(b[0]));
443
445
  for (const [filePath, backendSha] of sortedBackend) {
444
446
  const diskSha = diskComputed.checksums.get(filePath);
445
447
  if (diskSha) {
@@ -456,7 +458,7 @@ export class AgentSync {
456
458
  }
457
459
  const sortedDisk = [...diskComputed.checksums.entries()].sort((a, b) => a[0].localeCompare(b[0]));
458
460
  for (const [filePath, diskSha] of sortedDisk) {
459
- if (!backendResult.checksums.has(filePath) && isUserCreatedFile(filePath)) {
461
+ if (!filteredBackend.has(filePath) && isUserCreatedFile(filePath)) {
460
462
  result.push({ path: filePath, status: 'disk-only', diskSha });
461
463
  }
462
464
  }
@@ -578,6 +580,49 @@ export class AgentSync {
578
580
  return null;
579
581
  }
580
582
  }
583
+ /**
584
+ * Pull files from the backend DB and write them to disk.
585
+ * This fetches ALL project files and overwrites local versions.
586
+ * Files that exist only on disk (not in DB) are left untouched.
587
+ */
588
+ async pullFromBackend() {
589
+ const url = `${this.backendUrl}/api/sandbox/files?projectId=${this.projectId}`;
590
+ const response = await fetch(url);
591
+ if (!response.ok) {
592
+ throw new Error(`Failed to fetch files from backend: ${response.status}`);
593
+ }
594
+ const data = await response.json();
595
+ if (!data.files || data.files.length === 0) {
596
+ return { success: true, filesWritten: 0 };
597
+ }
598
+ // Ensure directories exist
599
+ const dirsToCreate = new Set();
600
+ for (const file of data.files) {
601
+ const filePath = path.join(this.projectDir, file.path);
602
+ dirsToCreate.add(path.dirname(filePath));
603
+ }
604
+ for (const dir of dirsToCreate) {
605
+ await fs.mkdir(dir, { recursive: true }).catch(() => { });
606
+ }
607
+ // Write files in parallel batches
608
+ const BATCH_SIZE = 50;
609
+ let written = 0;
610
+ for (let i = 0; i < data.files.length; i += BATCH_SIZE) {
611
+ const batch = data.files.slice(i, i + BATCH_SIZE);
612
+ await Promise.all(batch.map(async (file) => {
613
+ const filePath = path.join(this.projectDir, file.path);
614
+ await fs.writeFile(filePath, file.content, 'utf-8');
615
+ }));
616
+ written += batch.length;
617
+ }
618
+ // Invalidate caches and re-establish baseline
619
+ this.diskChecksumCache = null;
620
+ this.backendChecksumCache = null;
621
+ const versionId = String(data.versionId);
622
+ this.markSynced(versionId);
623
+ console.log(` [AgentSync] Pulled ${written} files from backend (version ${versionId})`);
624
+ return { success: true, filesWritten: written, versionId };
625
+ }
581
626
  // ─── Cleanup ──────────────────────────────────────────────
582
627
  destroy() {
583
628
  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.28');
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.28';
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.28",
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": {