nstantpage-agent 0.8.5 → 0.8.7
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/agentSync.d.ts +7 -9
- package/dist/agentSync.js +117 -101
- package/dist/commands/start.js +39 -12
- package/package.json +1 -1
package/dist/agentSync.d.ts
CHANGED
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* CORE PRINCIPLE: Disk is ALWAYS the source of truth. ONE-WAY only.
|
|
5
5
|
*
|
|
6
|
-
* When
|
|
7
|
-
*
|
|
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
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
* 1. File watcher detects changes (debounced 1s)
|
|
11
|
-
* 2. Accumulate changed file paths in pendingChangedFiles
|
|
12
|
-
* 3. Fire onSyncDirty → frontend receives notification
|
|
13
|
-
* 4. Frontend calls pushToBackend → agent reads pending files from disk → pushes to DB
|
|
11
|
+
* No checksums, no comparisons, no diffs. Disk wins. Always.
|
|
14
12
|
*/
|
|
15
13
|
export type SyncDirection = 'in-sync' | 'backend-ahead' | 'disk-ahead' | 'diverged';
|
|
16
14
|
export interface SyncStatusResult {
|
|
@@ -49,9 +47,9 @@ export declare class AgentSync {
|
|
|
49
47
|
private fileWatcher;
|
|
50
48
|
private debounceTimer;
|
|
51
49
|
private pendingChangedFiles;
|
|
52
|
-
private
|
|
50
|
+
private syncing;
|
|
53
51
|
private lastVersionId;
|
|
54
|
-
private
|
|
52
|
+
private lastSyncAt;
|
|
55
53
|
constructor(options: AgentSyncOptions);
|
|
56
54
|
startFileWatcher(): void;
|
|
57
55
|
stopFileWatcher(): void;
|
package/dist/agentSync.js
CHANGED
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* CORE PRINCIPLE: Disk is ALWAYS the source of truth. ONE-WAY only.
|
|
5
5
|
*
|
|
6
|
-
* When
|
|
7
|
-
*
|
|
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
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
* 1. File watcher detects changes (debounced 1s)
|
|
11
|
-
* 2. Accumulate changed file paths in pendingChangedFiles
|
|
12
|
-
* 3. Fire onSyncDirty → frontend receives notification
|
|
13
|
-
* 4. Frontend calls pushToBackend → agent reads pending files from disk → pushes to DB
|
|
11
|
+
* No checksums, no comparisons, no diffs. Disk wins. Always.
|
|
14
12
|
*/
|
|
15
13
|
import path from 'path';
|
|
16
14
|
import fs from 'fs/promises';
|
|
@@ -23,13 +21,20 @@ const SKIP_DIRS = new Set([
|
|
|
23
21
|
]);
|
|
24
22
|
const SKIP_FILES = new Set([
|
|
25
23
|
'.DS_Store', 'Thumbs.db', '.diagramgpt-meta.json',
|
|
26
|
-
'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock',
|
|
27
|
-
'.npmrc', '.gitignore', 'README.md',
|
|
28
24
|
]);
|
|
29
25
|
const SKIP_EXTENSIONS = new Set([
|
|
30
|
-
'.map', '.log', '.tmp', '.swp', '.swo',
|
|
31
|
-
'.
|
|
26
|
+
'.map', '.log', '.tmp', '.swp', '.swo',
|
|
27
|
+
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.bmp', '.tiff',
|
|
28
|
+
'.mp4', '.mp3', '.wav', '.ogg', '.webm', '.avi',
|
|
29
|
+
'.zip', '.tar', '.gz', '.rar', '.7z',
|
|
30
|
+
'.woff', '.woff2', '.ttf', '.eot', '.otf',
|
|
31
|
+
'.pdf', '.doc', '.docx', '.xls', '.xlsx',
|
|
32
|
+
'.exe', '.dll', '.so', '.dylib',
|
|
33
|
+
'.sqlite', '.db',
|
|
34
|
+
'.svg',
|
|
32
35
|
]);
|
|
36
|
+
/** Max file size to sync (1MB) */
|
|
37
|
+
const MAX_FILE_SIZE = 1_048_576;
|
|
33
38
|
function shouldSkip(name, isDir) {
|
|
34
39
|
if (isDir)
|
|
35
40
|
return SKIP_DIRS.has(name) || name.startsWith('.');
|
|
@@ -37,7 +42,7 @@ function shouldSkip(name, isDir) {
|
|
|
37
42
|
return true;
|
|
38
43
|
if (name.startsWith('.'))
|
|
39
44
|
return true;
|
|
40
|
-
const ext = path.extname(name);
|
|
45
|
+
const ext = path.extname(name).toLowerCase();
|
|
41
46
|
return SKIP_EXTENSIONS.has(ext);
|
|
42
47
|
}
|
|
43
48
|
/** Check if a relative path should be skipped (checks every path component) */
|
|
@@ -49,6 +54,42 @@ function shouldSkipPath(relPath) {
|
|
|
49
54
|
}
|
|
50
55
|
return shouldSkip(parts[parts.length - 1], false);
|
|
51
56
|
}
|
|
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
|
+
}
|
|
52
93
|
export class AgentSync {
|
|
53
94
|
projectDir;
|
|
54
95
|
projectId;
|
|
@@ -58,9 +99,9 @@ export class AgentSync {
|
|
|
58
99
|
fileWatcher = null;
|
|
59
100
|
debounceTimer = null;
|
|
60
101
|
pendingChangedFiles = new Set();
|
|
61
|
-
|
|
102
|
+
syncing = false;
|
|
62
103
|
lastVersionId = null;
|
|
63
|
-
|
|
104
|
+
lastSyncAt = null;
|
|
64
105
|
constructor(options) {
|
|
65
106
|
this.projectDir = options.projectDir;
|
|
66
107
|
this.projectId = options.projectId;
|
|
@@ -86,7 +127,6 @@ export class AgentSync {
|
|
|
86
127
|
const count = this.pendingChangedFiles.size;
|
|
87
128
|
const preview = [...this.pendingChangedFiles].slice(0, 5).join(', ');
|
|
88
129
|
console.log(` [AgentSync] Change detected: ${count} file(s) — ${preview}${count > 5 ? '...' : ''}`);
|
|
89
|
-
// Notify frontend so it triggers auto-push
|
|
90
130
|
if (this.onSyncDirty) {
|
|
91
131
|
this.onSyncDirty(this.projectId);
|
|
92
132
|
}
|
|
@@ -112,10 +152,10 @@ export class AgentSync {
|
|
|
112
152
|
this.debounceTimer = null;
|
|
113
153
|
}
|
|
114
154
|
}
|
|
115
|
-
// ─── Sync Status
|
|
155
|
+
// ─── Sync Status ─────────────────────────────────────────
|
|
116
156
|
async getSyncStatus() {
|
|
117
157
|
const pending = this.pendingChangedFiles.size;
|
|
118
|
-
const isDirty = pending > 0
|
|
158
|
+
const isDirty = pending > 0 && !this.syncing;
|
|
119
159
|
return {
|
|
120
160
|
inSync: !isDirty,
|
|
121
161
|
direction: isDirty ? 'disk-ahead' : 'in-sync',
|
|
@@ -127,7 +167,7 @@ export class AgentSync {
|
|
|
127
167
|
diskOnlyFiles: [],
|
|
128
168
|
backendOnlyFiles: [],
|
|
129
169
|
lastSyncedVersionId: this.lastVersionId,
|
|
130
|
-
lastSyncedAt: this.
|
|
170
|
+
lastSyncedAt: this.lastSyncAt,
|
|
131
171
|
diskDirty: isDirty,
|
|
132
172
|
computedAt: Date.now(),
|
|
133
173
|
};
|
|
@@ -139,114 +179,93 @@ export class AgentSync {
|
|
|
139
179
|
status: 'modified',
|
|
140
180
|
}));
|
|
141
181
|
}
|
|
142
|
-
// ─── Push to Backend
|
|
182
|
+
// ─── Push to Backend (Full Sync) ──────────────────────────
|
|
143
183
|
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
184
|
+
// Does EXACTLY what `nstantpage sync` does:
|
|
185
|
+
// 1. POST /api/projects/{id}/sync — new version + clear ALL FullFiles
|
|
186
|
+
// 2. Collect ALL files from disk
|
|
187
|
+
// 3. POST /api/sandbox/push-files — push ALL files in batches of 100
|
|
147
188
|
async pushToBackend() {
|
|
148
|
-
|
|
149
|
-
if (filesToProcess.length === 0) {
|
|
189
|
+
if (this.pendingChangedFiles.size === 0) {
|
|
150
190
|
return {
|
|
151
|
-
success: true,
|
|
152
|
-
|
|
153
|
-
filesDeleted: 0,
|
|
154
|
-
modifiedFiles: [],
|
|
155
|
-
addedFiles: [],
|
|
156
|
-
deletedFiles: [],
|
|
191
|
+
success: true, filesPushed: 0, filesDeleted: 0,
|
|
192
|
+
modifiedFiles: [], addedFiles: [], deletedFiles: [],
|
|
157
193
|
message: 'Already in sync — no changes to push',
|
|
158
194
|
};
|
|
159
195
|
}
|
|
160
|
-
if (this.
|
|
196
|
+
if (this.syncing) {
|
|
161
197
|
return {
|
|
162
|
-
success: true,
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
modifiedFiles: [],
|
|
166
|
-
addedFiles: [],
|
|
167
|
-
deletedFiles: [],
|
|
168
|
-
message: 'Push already in progress',
|
|
198
|
+
success: true, filesPushed: 0, filesDeleted: 0,
|
|
199
|
+
modifiedFiles: [], addedFiles: [], deletedFiles: [],
|
|
200
|
+
message: 'Sync already in progress',
|
|
169
201
|
};
|
|
170
202
|
}
|
|
171
|
-
this.
|
|
203
|
+
this.syncing = true;
|
|
172
204
|
try {
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
for (const relPath of filesToProcess) {
|
|
176
|
-
const fullPath = path.join(this.projectDir, relPath);
|
|
177
|
-
try {
|
|
178
|
-
const content = await fs.readFile(fullPath, 'utf-8');
|
|
179
|
-
files[relPath] = content;
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
// File doesn't exist or is binary/unreadable
|
|
183
|
-
if (!existsSync(fullPath)) {
|
|
184
|
-
deleted.push(relPath);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
const totalChanges = Object.keys(files).length + deleted.length;
|
|
189
|
-
if (totalChanges === 0) {
|
|
190
|
-
// All pending files were binary/unreadable — clear them
|
|
191
|
-
for (const f of filesToProcess)
|
|
192
|
-
this.pendingChangedFiles.delete(f);
|
|
193
|
-
return {
|
|
194
|
-
success: true,
|
|
195
|
-
filesPushed: 0,
|
|
196
|
-
filesDeleted: 0,
|
|
197
|
-
modifiedFiles: [],
|
|
198
|
-
addedFiles: [],
|
|
199
|
-
deletedFiles: [],
|
|
200
|
-
message: 'No pushable changes',
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
const pushUrl = `${this.backendUrl}/api/sandbox/push-files`;
|
|
204
|
-
const response = await fetch(pushUrl, {
|
|
205
|
+
// Step 1: Create new version + clear all FullFiles
|
|
206
|
+
const syncRes = await fetch(`${this.backendUrl}/api/projects/${this.projectId}/sync`, {
|
|
205
207
|
method: 'POST',
|
|
206
208
|
headers: { 'Content-Type': 'application/json' },
|
|
207
|
-
body: JSON.stringify({
|
|
208
|
-
projectId: parseInt(this.projectId, 10),
|
|
209
|
-
files,
|
|
210
|
-
deletedFiles: deleted,
|
|
211
|
-
}),
|
|
212
209
|
});
|
|
213
|
-
if (!
|
|
214
|
-
const
|
|
215
|
-
throw new Error(`
|
|
210
|
+
if (!syncRes.ok) {
|
|
211
|
+
const text = await syncRes.text().catch(() => '');
|
|
212
|
+
throw new Error(`Sync init failed (${syncRes.status}): ${text}`);
|
|
213
|
+
}
|
|
214
|
+
const syncData = await syncRes.json();
|
|
215
|
+
console.log(` [AgentSync] New version ${syncData.versionNumber} created (old files cleared)`);
|
|
216
|
+
// Step 2: Collect ALL files from disk
|
|
217
|
+
const allFiles = await collectAllFiles(this.projectDir, this.projectDir);
|
|
218
|
+
// Step 3: Push in batches of 100
|
|
219
|
+
const BATCH_SIZE = 100;
|
|
220
|
+
let totalPushed = 0;
|
|
221
|
+
for (let i = 0; i < allFiles.length; i += BATCH_SIZE) {
|
|
222
|
+
const batch = allFiles.slice(i, i + BATCH_SIZE);
|
|
223
|
+
const filesMap = {};
|
|
224
|
+
for (const f of batch) {
|
|
225
|
+
filesMap[f.relativePath] = f.content;
|
|
226
|
+
}
|
|
227
|
+
const pushRes = await fetch(`${this.backendUrl}/api/sandbox/push-files`, {
|
|
228
|
+
method: 'POST',
|
|
229
|
+
headers: { 'Content-Type': 'application/json' },
|
|
230
|
+
body: JSON.stringify({
|
|
231
|
+
projectId: parseInt(this.projectId, 10),
|
|
232
|
+
files: filesMap,
|
|
233
|
+
}),
|
|
234
|
+
});
|
|
235
|
+
if (!pushRes.ok) {
|
|
236
|
+
const text = await pushRes.text().catch(() => '');
|
|
237
|
+
throw new Error(`Push batch failed (${pushRes.status}): ${text}`);
|
|
238
|
+
}
|
|
239
|
+
totalPushed += batch.length;
|
|
216
240
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
this.lastPushAt = Date.now();
|
|
224
|
-
const fileNames = Object.keys(files);
|
|
225
|
-
console.log(` [AgentSync] Pushed ${fileNames.length} files, deleted ${deleted.length}`);
|
|
241
|
+
// Done — clear pending
|
|
242
|
+
this.pendingChangedFiles.clear();
|
|
243
|
+
this.lastVersionId = String(syncData.versionId);
|
|
244
|
+
this.lastSyncAt = Date.now();
|
|
245
|
+
const fileNames = allFiles.map(f => f.relativePath);
|
|
246
|
+
console.log(` [AgentSync] Full sync complete: ${totalPushed} files pushed (version ${syncData.versionNumber})`);
|
|
226
247
|
return {
|
|
227
248
|
success: true,
|
|
228
|
-
filesPushed:
|
|
229
|
-
filesDeleted:
|
|
249
|
+
filesPushed: totalPushed,
|
|
250
|
+
filesDeleted: 0,
|
|
230
251
|
modifiedFiles: fileNames,
|
|
231
252
|
addedFiles: [],
|
|
232
|
-
deletedFiles:
|
|
233
|
-
versionId:
|
|
253
|
+
deletedFiles: [],
|
|
254
|
+
versionId: String(syncData.versionId),
|
|
234
255
|
};
|
|
235
256
|
}
|
|
236
257
|
catch (err) {
|
|
237
|
-
|
|
238
|
-
console.error(` [AgentSync] Push failed:`, err.message);
|
|
258
|
+
console.error(` [AgentSync] Sync failed:`, err.message);
|
|
239
259
|
throw err;
|
|
240
260
|
}
|
|
241
261
|
finally {
|
|
242
|
-
this.
|
|
262
|
+
this.syncing = false;
|
|
243
263
|
}
|
|
244
264
|
}
|
|
245
265
|
// ─── Sync Markers (for compatibility with start.ts) ───────
|
|
246
266
|
markSynced(versionId) {
|
|
247
267
|
this.lastVersionId = versionId;
|
|
248
|
-
this.
|
|
249
|
-
// Clear any pending changes — we just synced, don't re-push fetched files
|
|
268
|
+
this.lastSyncAt = Date.now();
|
|
250
269
|
this.pendingChangedFiles.clear();
|
|
251
270
|
if (this.debounceTimer) {
|
|
252
271
|
clearTimeout(this.debounceTimer);
|
|
@@ -281,7 +300,6 @@ export class AgentSync {
|
|
|
281
300
|
if (!data.files || data.files.length === 0) {
|
|
282
301
|
return { success: true, filesWritten: 0 };
|
|
283
302
|
}
|
|
284
|
-
// Ensure directories exist
|
|
285
303
|
const dirsToCreate = new Set();
|
|
286
304
|
for (const file of data.files) {
|
|
287
305
|
const fp = path.join(this.projectDir, file.path);
|
|
@@ -290,7 +308,6 @@ export class AgentSync {
|
|
|
290
308
|
for (const dir of dirsToCreate) {
|
|
291
309
|
await fs.mkdir(dir, { recursive: true }).catch(() => { });
|
|
292
310
|
}
|
|
293
|
-
// Write files in parallel batches
|
|
294
311
|
const BATCH_SIZE = 50;
|
|
295
312
|
let written = 0;
|
|
296
313
|
for (let i = 0; i < data.files.length; i += BATCH_SIZE) {
|
|
@@ -301,9 +318,8 @@ export class AgentSync {
|
|
|
301
318
|
}));
|
|
302
319
|
written += batch.length;
|
|
303
320
|
}
|
|
304
|
-
// Clear pending changes from the writes and mark synced
|
|
305
321
|
this.lastVersionId = String(data.versionId);
|
|
306
|
-
this.
|
|
322
|
+
this.lastSyncAt = Date.now();
|
|
307
323
|
this.pendingChangedFiles.clear();
|
|
308
324
|
if (this.debounceTimer) {
|
|
309
325
|
clearTimeout(this.debounceTimer);
|
package/dist/commands/start.js
CHANGED
|
@@ -268,13 +268,16 @@ export async function startCommand(directory, options) {
|
|
|
268
268
|
console.log(chalk.gray(` API server: port ${apiPort}`));
|
|
269
269
|
console.log(chalk.gray(` Gateway: ${options.gateway}`));
|
|
270
270
|
console.log(chalk.gray(` Backend: ${backendUrl}\n`));
|
|
271
|
-
//
|
|
271
|
+
// Determine if this is a user-managed local directory (--dir flag or explicit path)
|
|
272
|
+
// For local projects: disk is truth, NEVER overwrite disk from DB
|
|
273
|
+
const isLocalDir = !!(options.dir) || directory !== '.';
|
|
274
|
+
// 1. Fetch or sync project files
|
|
272
275
|
const installer = new PackageInstaller({ projectDir });
|
|
273
276
|
let fetchedVersionId;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
//
|
|
277
|
+
if (isLocalDir) {
|
|
278
|
+
// Local project — SKIP DB→disk fetch. Disk is truth.
|
|
279
|
+
console.log(chalk.gray(` Local project — using files from disk (not fetching from DB)`));
|
|
280
|
+
// Install dependencies if needed
|
|
278
281
|
if (!installer.areDependenciesInstalled()) {
|
|
279
282
|
if (fs.existsSync(path.join(projectDir, 'package.json'))) {
|
|
280
283
|
console.log(chalk.gray(' Installing dependencies...'));
|
|
@@ -292,13 +295,37 @@ export async function startCommand(directory, options) {
|
|
|
292
295
|
console.log(chalk.gray(` Dependencies already installed`));
|
|
293
296
|
}
|
|
294
297
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
298
|
+
else {
|
|
299
|
+
// Cloud/managed project — fetch files from DB to disk
|
|
300
|
+
try {
|
|
301
|
+
const { fileCount, isNew, versionId } = await fetchProjectFiles(backendUrl, projectId, projectDir, token);
|
|
302
|
+
fetchedVersionId = versionId;
|
|
303
|
+
// 2. Install dependencies if needed (verifies actual packages, not just folder)
|
|
304
|
+
if (!installer.areDependenciesInstalled()) {
|
|
305
|
+
if (fs.existsSync(path.join(projectDir, 'package.json'))) {
|
|
306
|
+
console.log(chalk.gray(' Installing dependencies...'));
|
|
307
|
+
try {
|
|
308
|
+
await installer.ensureDependencies();
|
|
309
|
+
console.log(chalk.green(` ✓ Dependencies installed`));
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
console.log(chalk.yellow(` ⚠ Install failed: ${err.message?.slice(0, 200)}`));
|
|
313
|
+
console.log(chalk.gray(` Will retry when dev server starts`));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
console.log(chalk.gray(` Dependencies already installed`));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
console.log(chalk.yellow(` ⚠ Could not fetch project files: ${err.message}`));
|
|
323
|
+
console.log(chalk.gray(' Continuing with existing local files (if any)...'));
|
|
324
|
+
if (!fs.existsSync(path.join(projectDir, 'package.json'))) {
|
|
325
|
+
console.log(chalk.red(`\n✗ No package.json found and cannot fetch files from backend.`));
|
|
326
|
+
console.log(chalk.gray(' Check your project ID and authentication.'));
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
302
329
|
}
|
|
303
330
|
}
|
|
304
331
|
// Save .nstantpage.json for future runs
|
package/package.json
CHANGED