@makerbi/remodex 1.4.0 → 1.5.0

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.
@@ -37,6 +37,8 @@ const IMAGE_MIME_TYPES_BY_EXTENSION = new Map([
37
37
  [".heic", "image/heic"],
38
38
  [".heif", "image/heif"],
39
39
  ]);
40
+ /** Match git-handler.js: Node default maxBuffer is 1 MiB. */
41
+ const GIT_EXEC_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
40
42
  const repoMutationLocks = new Map();
41
43
 
42
44
  function handleWorkspaceRequest(rawMessage, sendResponse) {
@@ -100,6 +102,10 @@ async function handleWorkspaceMethod(method, params) {
100
102
  return workspaceRevertPatchPreview(repoRoot, params);
101
103
  case "workspace/revertPatchApply":
102
104
  return withRepoMutationLock(repoRoot, () => workspaceRevertPatchApply(repoRoot, params));
105
+ case "workspace/revertPatchBatchPreview":
106
+ return workspaceRevertPatchBatchPreview(repoRoot, params);
107
+ case "workspace/revertPatchBatchApply":
108
+ return withRepoMutationLock(repoRoot, () => workspaceRevertPatchBatchApply(repoRoot, params));
103
109
  default:
104
110
  throw workspaceError("unknown_method", `Unknown workspace method: ${method}`);
105
111
  }
@@ -367,7 +373,7 @@ async function workspaceRevertPatchPreview(repoRoot, params) {
367
373
  };
368
374
  }
369
375
 
370
- const applyCheck = await runGitApply(repoRoot, ["apply", "--reverse", "--check"], forwardPatch);
376
+ const applyCheck = await checkReversePatch(repoRoot, forwardPatch);
371
377
  const conflicts = applyCheck.ok
372
378
  ? []
373
379
  : parseApplyConflicts(applyCheck.stderr || applyCheck.stdout || "Patch does not apply.");
@@ -383,6 +389,7 @@ async function workspaceRevertPatchPreview(repoRoot, params) {
383
389
 
384
390
  // Reverse-applies the patch only after the same safety checks pass in the locked mutation path.
385
391
  async function workspaceRevertPatchApply(repoRoot, params) {
392
+ const forwardPatch = resolveForwardPatch(params);
386
393
  const preview = await workspaceRevertPatchPreview(repoRoot, params);
387
394
  if (!preview.canRevert) {
388
395
  return {
@@ -394,8 +401,10 @@ async function workspaceRevertPatchApply(repoRoot, params) {
394
401
  };
395
402
  }
396
403
 
397
- const forwardPatch = resolveForwardPatch(params);
398
- const applyResult = await runGitApply(repoRoot, ["apply", "--reverse"], forwardPatch);
404
+ const checkedPatch = await checkReversePatch(repoRoot, forwardPatch);
405
+ const applyResult = checkedPatch.ok
406
+ ? await runGitApply(repoRoot, checkedPatch.applyArgs, forwardPatch)
407
+ : checkedPatch;
399
408
  if (!applyResult.ok) {
400
409
  return {
401
410
  success: false,
@@ -407,6 +416,7 @@ async function workspaceRevertPatchApply(repoRoot, params) {
407
416
  };
408
417
  }
409
418
 
419
+ await resetTargetedFilesIndex(repoRoot, preview.affectedFiles);
410
420
  const status = await gitStatus(repoRoot).catch(() => null);
411
421
  return {
412
422
  success: true,
@@ -418,6 +428,91 @@ async function workspaceRevertPatchApply(repoRoot, params) {
418
428
  };
419
429
  }
420
430
 
431
+ // Validates a newest-first patch batch as one reverse operation so dependent patches see the right state.
432
+ async function workspaceRevertPatchBatchPreview(repoRoot, params) {
433
+ const patches = resolveForwardPatchBatch(params);
434
+ const analyses = patches.map((patch) => analyzeUnifiedPatch(patch.forwardPatch));
435
+ const affectedFiles = uniqueSorted(analyses.flatMap((analysis) => analysis.affectedFiles));
436
+ const unsupportedReasons = uniqueSorted(analyses.flatMap((analysis) => analysis.unsupportedReasons));
437
+ const stagedFiles = await findStagedTargetedFiles(repoRoot, affectedFiles);
438
+
439
+ if (unsupportedReasons.length || stagedFiles.length) {
440
+ return {
441
+ canRevert: false,
442
+ affectedFiles,
443
+ conflicts: [],
444
+ unsupportedReasons,
445
+ stagedFiles,
446
+ patchResults: patches.map((patch, index) => ({
447
+ id: patch.id,
448
+ canRevert: analyses[index].unsupportedReasons.length === 0,
449
+ unsupportedReasons: analyses[index].unsupportedReasons,
450
+ })),
451
+ };
452
+ }
453
+
454
+ const sequenceCheck = await previewReversePatchSequence(repoRoot, patches, affectedFiles);
455
+ const conflicts = sequenceCheck.ok
456
+ ? []
457
+ : parseApplyConflicts(sequenceCheck.stderr || sequenceCheck.stdout || "Patch batch does not apply.");
458
+
459
+ return {
460
+ canRevert: sequenceCheck.ok && conflicts.length === 0,
461
+ affectedFiles,
462
+ conflicts,
463
+ unsupportedReasons: [],
464
+ stagedFiles,
465
+ patchResults: patches.map((patch) => ({
466
+ id: patch.id,
467
+ canRevert: sequenceCheck.ok && conflicts.length === 0,
468
+ })),
469
+ };
470
+ }
471
+
472
+ // Applies all batch patches under one repo lock and only marks success after the full reverse patch lands.
473
+ async function workspaceRevertPatchBatchApply(repoRoot, params) {
474
+ const patches = resolveForwardPatchBatch(params);
475
+ const preview = await workspaceRevertPatchBatchPreview(repoRoot, params);
476
+ if (!preview.canRevert) {
477
+ return {
478
+ success: false,
479
+ revertedFiles: [],
480
+ conflicts: preview.conflicts,
481
+ unsupportedReasons: preview.unsupportedReasons,
482
+ stagedFiles: preview.stagedFiles,
483
+ patchResults: preview.patchResults || [],
484
+ };
485
+ }
486
+
487
+ const applyResult = await applyReversePatchSequence(repoRoot, patches, preview.affectedFiles);
488
+ if (!applyResult.ok) {
489
+ return {
490
+ success: false,
491
+ revertedFiles: [],
492
+ conflicts: parseApplyConflicts(applyResult.stderr || applyResult.stdout || "Patch batch does not apply."),
493
+ unsupportedReasons: [],
494
+ stagedFiles: [],
495
+ patchResults: patches.map((patch) => ({
496
+ id: patch.id,
497
+ applied: applyResult.appliedPatchIds.includes(patch.id),
498
+ })),
499
+ status: await gitStatus(repoRoot).catch(() => null),
500
+ };
501
+ }
502
+
503
+ await resetTargetedFilesIndex(repoRoot, preview.affectedFiles);
504
+ const status = await gitStatus(repoRoot).catch(() => null);
505
+ return {
506
+ success: true,
507
+ revertedFiles: preview.affectedFiles,
508
+ conflicts: [],
509
+ unsupportedReasons: [],
510
+ stagedFiles: [],
511
+ patchResults: patches.map((patch) => ({ id: patch.id, applied: true })),
512
+ status,
513
+ };
514
+ }
515
+
421
516
  function resolveForwardPatch(params) {
422
517
  const forwardPatch =
423
518
  typeof params.forwardPatch === "string" ? params.forwardPatch : "";
@@ -429,6 +524,32 @@ function resolveForwardPatch(params) {
429
524
  return forwardPatch.endsWith("\n") ? forwardPatch : `${forwardPatch}\n`;
430
525
  }
431
526
 
527
+ function resolveForwardPatchBatch(params) {
528
+ const rawPatches = Array.isArray(params.patches) ? params.patches : [];
529
+ const patches = rawPatches.map((rawPatch, index) => {
530
+ if (typeof rawPatch === "string") {
531
+ return {
532
+ id: String(index),
533
+ forwardPatch: rawPatch.endsWith("\n") ? rawPatch : `${rawPatch}\n`,
534
+ };
535
+ }
536
+
537
+ const forwardPatch = rawPatch && typeof rawPatch.forwardPatch === "string"
538
+ ? rawPatch.forwardPatch
539
+ : "";
540
+ return {
541
+ id: rawPatch && typeof rawPatch.id === "string" ? rawPatch.id : String(index),
542
+ forwardPatch: forwardPatch.endsWith("\n") ? forwardPatch : `${forwardPatch}\n`,
543
+ };
544
+ }).filter((patch) => patch.forwardPatch.trim());
545
+
546
+ if (!patches.length) {
547
+ throw workspaceError("missing_patch", "The request must include at least one non-empty patch.");
548
+ }
549
+
550
+ return patches;
551
+ }
552
+
432
553
  function analyzeUnifiedPatch(rawPatch) {
433
554
  const patch = rawPatch.trim();
434
555
  if (!patch) {
@@ -585,6 +706,188 @@ async function findStagedTargetedFiles(cwd, affectedFiles) {
585
706
  }
586
707
  }
587
708
 
709
+ async function previewReversePatchSequence(repoRoot, patches, affectedFiles) {
710
+ const sandboxRoot = await createPatchSandbox(repoRoot, affectedFiles);
711
+
712
+ try {
713
+ for (const patch of patches) {
714
+ const check = await checkReversePatch(sandboxRoot, patch.forwardPatch);
715
+ if (!check.ok) {
716
+ return { ...check, failedPatchId: patch.id };
717
+ }
718
+
719
+ const applied = await runGitApply(sandboxRoot, check.applyArgs, patch.forwardPatch);
720
+ if (!applied.ok) {
721
+ return { ...applied, failedPatchId: patch.id };
722
+ }
723
+ await syncPatchSandboxIndex(sandboxRoot);
724
+ }
725
+
726
+ return { ok: true, stdout: "", stderr: "" };
727
+ } finally {
728
+ await fs.promises.rm(sandboxRoot, { recursive: true, force: true }).catch(() => {});
729
+ }
730
+ }
731
+
732
+ async function applyReversePatchSequence(repoRoot, patches, affectedFiles) {
733
+ const appliedPatchIds = [];
734
+ const backup = await createPatchBackup(repoRoot, affectedFiles);
735
+
736
+ try {
737
+ for (const patch of patches) {
738
+ const checkedPatch = await checkReversePatch(repoRoot, patch.forwardPatch);
739
+ if (!checkedPatch.ok) {
740
+ await restorePatchBackup(repoRoot, backup);
741
+ return { ...checkedPatch, appliedPatchIds, failedPatchId: patch.id };
742
+ }
743
+
744
+ const appliedPatch = await runGitApply(repoRoot, checkedPatch.applyArgs, patch.forwardPatch);
745
+ if (!appliedPatch.ok) {
746
+ await restorePatchBackup(repoRoot, backup);
747
+ return { ...appliedPatch, appliedPatchIds, failedPatchId: patch.id };
748
+ }
749
+
750
+ appliedPatchIds.push(patch.id);
751
+ }
752
+
753
+ return { ok: true, stdout: "", stderr: "", appliedPatchIds };
754
+ } finally {
755
+ await fs.promises.rm(backup.root, { recursive: true, force: true }).catch(() => {});
756
+ }
757
+ }
758
+
759
+ async function createPatchSandbox(repoRoot, affectedFiles) {
760
+ const sandboxRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), "remodex-revert-preview-"));
761
+
762
+ for (const affectedFile of affectedFiles) {
763
+ const sourcePath = path.resolve(repoRoot, affectedFile);
764
+ if (!isPathInside(sourcePath, repoRoot)) {
765
+ continue;
766
+ }
767
+
768
+ const destinationPath = path.resolve(sandboxRoot, affectedFile);
769
+ if (!isPathInside(destinationPath, sandboxRoot) || !fs.existsSync(sourcePath)) {
770
+ continue;
771
+ }
772
+
773
+ await fs.promises.mkdir(path.dirname(destinationPath), { recursive: true });
774
+ await fs.promises.copyFile(sourcePath, destinationPath);
775
+ }
776
+
777
+ await initializePatchSandboxGitRepo(sandboxRoot);
778
+ return sandboxRoot;
779
+ }
780
+
781
+ async function initializePatchSandboxGitRepo(sandboxRoot) {
782
+ await git(sandboxRoot, "init", "-q");
783
+ await git(sandboxRoot, "config", "user.email", "remodex@example.local");
784
+ await git(sandboxRoot, "config", "user.name", "Remodex");
785
+ await syncPatchSandboxIndex(sandboxRoot);
786
+ await git(sandboxRoot, "commit", "-qm", "snapshot", "--allow-empty");
787
+ }
788
+
789
+ async function syncPatchSandboxIndex(sandboxRoot) {
790
+ await git(sandboxRoot, "add", "-A");
791
+ }
792
+
793
+ async function createPatchBackup(repoRoot, affectedFiles) {
794
+ const backupRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), "remodex-revert-backup-"));
795
+ const entries = [];
796
+
797
+ for (const affectedFile of affectedFiles) {
798
+ const sourcePath = path.resolve(repoRoot, affectedFile);
799
+ if (!isPathInside(sourcePath, repoRoot)) {
800
+ continue;
801
+ }
802
+
803
+ const backupPath = path.resolve(backupRoot, affectedFile);
804
+ if (!isPathInside(backupPath, backupRoot)) {
805
+ continue;
806
+ }
807
+
808
+ const exists = fs.existsSync(sourcePath);
809
+ entries.push({ relativePath: affectedFile, exists });
810
+ if (!exists) {
811
+ continue;
812
+ }
813
+
814
+ await fs.promises.mkdir(path.dirname(backupPath), { recursive: true });
815
+ await fs.promises.copyFile(sourcePath, backupPath);
816
+ }
817
+
818
+ return { root: backupRoot, entries };
819
+ }
820
+
821
+ async function restorePatchBackup(repoRoot, backup) {
822
+ for (const entry of backup.entries) {
823
+ const targetPath = path.resolve(repoRoot, entry.relativePath);
824
+ if (!isPathInside(targetPath, repoRoot)) {
825
+ continue;
826
+ }
827
+
828
+ if (!entry.exists) {
829
+ await fs.promises.rm(targetPath, { force: true, recursive: true }).catch(() => {});
830
+ continue;
831
+ }
832
+
833
+ const backupPath = path.resolve(backup.root, entry.relativePath);
834
+ if (!isPathInside(backupPath, backup.root)) {
835
+ continue;
836
+ }
837
+
838
+ await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
839
+ await fs.promises.copyFile(backupPath, targetPath);
840
+ }
841
+
842
+ await resetTargetedFilesIndex(repoRoot, backup.entries.map((entry) => entry.relativePath));
843
+ }
844
+
845
+ async function resetTargetedFilesIndex(cwd, affectedFiles) {
846
+ if (!affectedFiles.length) {
847
+ return;
848
+ }
849
+
850
+ await git(cwd, "reset", "-q", "--", ...affectedFiles);
851
+ }
852
+
853
+ async function checkReversePatch(cwd, patchText) {
854
+ if (isFileLifecyclePatch(patchText)) {
855
+ const plainCheck = await runGitApply(cwd, ["apply", "--reverse", "--check"], patchText);
856
+ return { ...plainCheck, applyArgs: ["apply", "--reverse"] };
857
+ }
858
+
859
+ const codexCheckArgs = ["apply", "--reverse", "--check", "--3way"];
860
+ const plainCheckArgs = ["apply", "--reverse", "--check"];
861
+ const codexCheck = await runGitApply(cwd, codexCheckArgs, patchText);
862
+
863
+ if (codexCheck.ok) {
864
+ return { ...codexCheck, applyArgs: ["apply", "--reverse", "--3way"] };
865
+ }
866
+
867
+ // git apply --3way requires index/worktree agreement; assistant edits are usually unstaged.
868
+ if (!isIndexMismatch(codexCheck)) {
869
+ return { ...codexCheck, applyArgs: ["apply", "--reverse", "--3way"] };
870
+ }
871
+
872
+ const plainCheck = await runGitApply(cwd, plainCheckArgs, patchText);
873
+ return { ...plainCheck, applyArgs: ["apply", "--reverse"] };
874
+ }
875
+
876
+ function isFileLifecyclePatch(patchText) {
877
+ return String(patchText || "")
878
+ .split("\n")
879
+ .some((line) => line === "--- /dev/null" || line === "+++ /dev/null");
880
+ }
881
+
882
+ function isIndexMismatch(result) {
883
+ const output = `${result.stderr || ""}\n${result.stdout || ""}`;
884
+ return output.includes("does not match index");
885
+ }
886
+
887
+ function uniqueSorted(values) {
888
+ return [...new Set(values.filter(Boolean))].sort();
889
+ }
890
+
588
891
  async function runGitApply(cwd, args, patchText) {
589
892
  const tempPatchPath = await writeTempPatchFile(patchText);
590
893
 
@@ -592,6 +895,7 @@ async function runGitApply(cwd, args, patchText) {
592
895
  const { stdout, stderr } = await execFileAsync("git", [...args, tempPatchPath], {
593
896
  cwd,
594
897
  timeout: GIT_TIMEOUT_MS,
898
+ maxBuffer: GIT_EXEC_MAX_BUFFER_BYTES,
595
899
  });
596
900
  return { ok: true, stdout, stderr };
597
901
  } catch (err) {
@@ -750,7 +1054,11 @@ function workspaceError(errorCode, userMessage) {
750
1054
  }
751
1055
 
752
1056
  function git(cwd, ...args) {
753
- return execFileAsync("git", args, { cwd, timeout: GIT_TIMEOUT_MS })
1057
+ return execFileAsync("git", args, {
1058
+ cwd,
1059
+ timeout: GIT_TIMEOUT_MS,
1060
+ maxBuffer: GIT_EXEC_MAX_BUFFER_BYTES,
1061
+ })
754
1062
  .then(({ stdout }) => stdout)
755
1063
  .catch((err) => {
756
1064
  const msg = (err.stderr || err.message || "").trim();
@@ -1,4 +0,0 @@
1
- {
2
- "relayUrl": "wss://remodex.vectorvein.com/relay",
3
- "pushServiceUrl": ""
4
- }