@micropage-sh/cli 2.0.7 → 2.0.8

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/supabase.js +55 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micropage-sh/cli",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "CLI for micropage.sh - create, sync, and publish microsites",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/supabase.js CHANGED
@@ -437,6 +437,12 @@ async function streamDeployEventsUntilDone(bearerToken, projectId, buildId, opti
437
437
  return { terminalEvent };
438
438
  }
439
439
 
440
+ function hashFile(filePath) {
441
+ const crypto = require('crypto');
442
+ const fs = require('fs');
443
+ return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
444
+ }
445
+
440
446
  /**
441
447
  * Upload a single asset file to the project via the upload-file Edge function.
442
448
  * Uses a pre-obtained Supabase access token.
@@ -447,10 +453,12 @@ async function uploadAssetWithToken(accessToken, projectId, filePath, filename)
447
453
 
448
454
  const bytes = fs.readFileSync(filePath);
449
455
  const mimeType = mime.fromFilename(filename);
456
+ const sha = hashFile(filePath);
450
457
 
451
458
  const formData = new FormData();
452
459
  formData.append('file', new Blob([bytes], { type: mimeType }), filename);
453
460
  formData.append('project_id', String(projectId));
461
+ formData.append('content_hash', sha);
454
462
 
455
463
  const res = await fetch(`${SUPABASE_URL}/functions/v1/upload-file`, {
456
464
  method: 'POST',
@@ -473,9 +481,33 @@ async function uploadAssetWithToken(accessToken, projectId, filePath, filename)
473
481
  return data;
474
482
  }
475
483
 
484
+ async function deleteFileWithToken(accessToken, fileId) {
485
+ const res = await fetch(`${SUPABASE_URL}/functions/v1/delete-file`, {
486
+ method: 'POST',
487
+ headers: {
488
+ 'apikey': SUPABASE_ANON_KEY,
489
+ 'Authorization': `Bearer ${accessToken}`,
490
+ 'Content-Type': 'application/json',
491
+ },
492
+ body: JSON.stringify({ file_id: fileId }),
493
+ });
494
+ const text = await res.text();
495
+ let data = null;
496
+ try { data = text ? JSON.parse(text) : null; } catch { data = { raw: text }; }
497
+ if (!res.ok) {
498
+ const msg = (data && (data.error || data.message)) || `HTTP ${res.status}`;
499
+ const err = new Error(msg);
500
+ err.status = res.status;
501
+ throw err;
502
+ }
503
+ return data;
504
+ }
505
+
476
506
  /**
477
507
  * Upload all assets from the local `assets/` directory for a project.
478
- * Skips files that already exist on the server (matched by filename).
508
+ * Compares each local file's SHA-256 hash against the server's stored hash.
509
+ * Skips files whose content is unchanged. Deletes and re-uploads files whose
510
+ * content has changed (or whose server hash is NULL from before hashing was added).
479
511
  * Returns the count of files uploaded.
480
512
  */
481
513
  async function uploadAssetsWithToken(accessToken, projectId, cwd, onProgress) {
@@ -485,7 +517,9 @@ async function uploadAssetsWithToken(accessToken, projectId, cwd, onProgress) {
485
517
  const assetFiles = listAssetsFromDir(cwd);
486
518
  if (assetFiles.length === 0) return 0;
487
519
 
488
- // Fetch existing filenames so we can skip unchanged files
520
+ // Fetch existing files so we can skip unchanged ones. We MUST treat a
521
+ // non-OK response as fatal: continuing with an empty list would re-upload
522
+ // every asset and (when filenames collide) leave duplicate user_files rows.
489
523
  const listRes = await fetch(
490
524
  `${SUPABASE_URL}/functions/v1/list-files?project_id=${encodeURIComponent(projectId)}`,
491
525
  {
@@ -495,13 +529,28 @@ async function uploadAssetsWithToken(accessToken, projectId, cwd, onProgress) {
495
529
  },
496
530
  },
497
531
  );
498
- const listData = listRes.ok ? await listRes.json() : null;
499
- const existing = new Set((listData?.files || []).map((f) => f.filename));
532
+ if (!listRes.ok) {
533
+ const text = await listRes.text().catch(() => '');
534
+ const err = new Error(`Failed to list project files: HTTP ${listRes.status}${text ? ` — ${text}` : ''}`);
535
+ err.status = listRes.status;
536
+ throw err;
537
+ }
538
+ const listData = await listRes.json();
539
+ const existingByName = new Map(
540
+ (listData?.files || []).map((f) => [f.filename, { id: f.id, content_hash: f.content_hash }]),
541
+ );
500
542
 
501
543
  let uploaded = 0;
502
544
  for (const filename of assetFiles) {
503
- if (existing.has(filename)) continue;
504
545
  const filePath = path.join(cwd, 'assets', filename);
546
+ const localHash = hashFile(filePath);
547
+ const existing = existingByName.get(filename);
548
+
549
+ if (existing && existing.content_hash === localHash) continue;
550
+
551
+ if (existing) {
552
+ await deleteFileWithToken(accessToken, existing.id);
553
+ }
505
554
  await uploadAssetWithToken(accessToken, projectId, filePath, filename);
506
555
  if (onProgress) onProgress(filename);
507
556
  uploaded++;
@@ -645,4 +694,5 @@ module.exports = {
645
694
  pushWithToken,
646
695
  invokePublishBuild,
647
696
  uploadAssetsWithToken,
697
+ hashFile,
648
698
  };