@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.
- package/package.json +6 -1
- package/src/bridge.js +425 -68
- package/src/desktop-handler.js +100 -11
- package/src/desktop-ipc-action-follower.js +42 -12
- package/src/git-handler.js +48 -4
- package/src/macos-launch-agent.js +2 -2
- package/src/pet-handler.js +537 -0
- package/src/qr.js +21 -2
- package/src/rollout-watch.js +17 -7
- package/src/workspace-handler.js +312 -4
- package/src/private-defaults.json +0 -4
package/src/workspace-handler.js
CHANGED
|
@@ -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
|
|
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
|
|
398
|
-
const applyResult =
|
|
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, {
|
|
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();
|