@makerbi/remodex 2.3.2 → 2.5.6
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/bin/remodex.js +25 -2
- package/package.json +1 -1
- package/src/bridge.js +80 -61
- package/src/codex-desktop-refresher.js +173 -8
- package/src/codex-tool-wrapper.js +523 -0
- package/src/desktop-ipc-action-follower.js +412 -129
- package/src/desktop-ipc-live-owner.js +148 -6
- package/src/desktop-ipc-owner-transport.js +143 -73
- package/src/desktop-ipc-shared.js +273 -46
- package/src/git-handler.js +189 -108
- package/src/index.js +2 -0
- package/src/macos-launch-agent.js +65 -34
- package/src/rollout-live-mirror.js +282 -22
- package/src/rollout-watch.js +176 -54
- package/src/session-jsonl-history.js +77 -33
- package/src/thread-list-provenance.js +105 -0
- package/src/thread-row-enrichment.js +43 -0
- package/src/thread-runtime-settings-store.js +3 -21
- package/src/worktree-origin.js +192 -0
package/src/git-handler.js
CHANGED
|
@@ -635,13 +635,11 @@ async function gitCreateBranch(cwd, params) {
|
|
|
635
635
|
return { branch: name, status };
|
|
636
636
|
}
|
|
637
637
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
await assertValidCreatedBranchName(cwd, branch);
|
|
644
|
-
|
|
638
|
+
// Resolves the shared creation context for both worktree flavors and enforces
|
|
639
|
+
// the invariants they have in common: a locally available base branch, and
|
|
640
|
+
// dirty changes that may only travel when the base branch is checked out.
|
|
641
|
+
// `destinationLabel` only shapes the dirty-transfer error message.
|
|
642
|
+
async function resolveWorktreeCreationContext(cwd, params, destinationLabel) {
|
|
645
643
|
const branchResult = await gitBranches(cwd);
|
|
646
644
|
const repoRoot = await resolveRepoRoot(cwd);
|
|
647
645
|
const status = await gitStatus(cwd);
|
|
@@ -666,34 +664,27 @@ async function gitCreateWorktree(cwd, params) {
|
|
|
666
664
|
const transferVerb = changeTransfer === "copy" ? "copy" : "move";
|
|
667
665
|
throw gitError(
|
|
668
666
|
"dirty_worktree_base_mismatch",
|
|
669
|
-
`Uncommitted changes can ${transferVerb} into
|
|
667
|
+
`Uncommitted changes can ${transferVerb} into ${destinationLabel} only from ${currentBranchLabel}. Switch the base branch to match or clean up local changes first.`
|
|
670
668
|
);
|
|
671
669
|
}
|
|
672
670
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
branch,
|
|
684
|
-
worktreePath: existingWorktreePath,
|
|
685
|
-
alreadyExisted: true,
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
const branchExists = await localBranchExists(cwd, branch);
|
|
690
|
-
if (branchExists) {
|
|
691
|
-
throw gitError(
|
|
692
|
-
"branch_exists",
|
|
693
|
-
`Branch '${branch}' already exists locally. Choose another name or open that branch instead.`
|
|
694
|
-
);
|
|
695
|
-
}
|
|
671
|
+
return {
|
|
672
|
+
branchResult,
|
|
673
|
+
repoRoot,
|
|
674
|
+
projectRelativePath,
|
|
675
|
+
changeScope,
|
|
676
|
+
baseBranch,
|
|
677
|
+
changeTransfer,
|
|
678
|
+
canCarryLocalChanges,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
696
681
|
|
|
682
|
+
// Shared creation core: allocates the managed path, carries local changes per
|
|
683
|
+
// the transfer mode, runs `git worktree add`, copies manifest files, and rolls
|
|
684
|
+
// everything back if any step after allocation fails. `branch` switches the
|
|
685
|
+
// checkout mode (`-b branch` vs `--detach`) and scopes cleanup/error mapping.
|
|
686
|
+
async function createWorktreeAtManagedPath(context, { branch = null, failureMessage }) {
|
|
687
|
+
const { repoRoot, changeScope, baseBranch, changeTransfer, canCarryLocalChanges } = context;
|
|
697
688
|
const worktreeRootPath = allocateManagedWorktreePath(repoRoot);
|
|
698
689
|
let handoffStashRef = null;
|
|
699
690
|
let copiedLocalChangesPatch = "";
|
|
@@ -708,7 +699,8 @@ async function gitCreateWorktree(cwd, params) {
|
|
|
708
699
|
}
|
|
709
700
|
}
|
|
710
701
|
|
|
711
|
-
|
|
702
|
+
const checkoutArgs = branch ? ["-b", branch] : ["--detach"];
|
|
703
|
+
await git(repoRoot, "worktree", "add", ...checkoutArgs, worktreeRootPath, baseBranch);
|
|
712
704
|
didCreateWorktree = true;
|
|
713
705
|
|
|
714
706
|
if (handoffStashRef) {
|
|
@@ -717,6 +709,7 @@ async function gitCreateWorktree(cwd, params) {
|
|
|
717
709
|
if (copiedLocalChangesPatch) {
|
|
718
710
|
await applyCopiedLocalChangesToWorktree(worktreeRootPath, copiedLocalChangesPatch);
|
|
719
711
|
}
|
|
712
|
+
await copyWorktreeIncludeFiles(repoRoot, worktreeRootPath);
|
|
720
713
|
} catch (err) {
|
|
721
714
|
if (didCreateWorktree) {
|
|
722
715
|
await cleanupManagedWorktree(repoRoot, worktreeRootPath, branch);
|
|
@@ -731,102 +724,90 @@ async function gitCreateWorktree(cwd, params) {
|
|
|
731
724
|
if (err.message?.includes("invalid reference")) {
|
|
732
725
|
throw gitError("missing_base_branch", `Base branch '${baseBranch}' does not exist.`);
|
|
733
726
|
}
|
|
734
|
-
if (
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
727
|
+
if (branch) {
|
|
728
|
+
if (err.message?.includes("already exists")) {
|
|
729
|
+
throw gitError("branch_exists", `Branch '${branch}' already exists.`);
|
|
730
|
+
}
|
|
731
|
+
if (err.message?.includes("already used by worktree") || err.message?.includes("already checked out at")) {
|
|
732
|
+
throw gitError(
|
|
733
|
+
"branch_in_other_worktree",
|
|
734
|
+
`Branch '${branch}' is already open in another worktree.`
|
|
735
|
+
);
|
|
736
|
+
}
|
|
742
737
|
}
|
|
743
|
-
throw gitError("create_worktree_failed", err.message ||
|
|
738
|
+
throw gitError("create_worktree_failed", err.message || failureMessage);
|
|
744
739
|
}
|
|
745
740
|
|
|
746
|
-
const worktreePath = scopedWorktreePath(worktreeRootPath, projectRelativePath);
|
|
747
741
|
return {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
alreadyExisted: false,
|
|
742
|
+
worktreeRootPath,
|
|
743
|
+
transferredChanges: Boolean(handoffStashRef || copiedLocalChangesPatch),
|
|
751
744
|
};
|
|
752
745
|
}
|
|
753
746
|
|
|
754
|
-
async function
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
const projectRelativePath = resolveProjectRelativePath(cwd, repoRoot);
|
|
759
|
-
const changeScope = await scopedProjectChanges(repoRoot, projectRelativePath);
|
|
760
|
-
const baseBranch = resolveBaseBranchName(params.baseBranch, branchResult.defaultBranch);
|
|
761
|
-
const changeTransfer = resolveWorktreeChangeTransfer(params.changeTransfer);
|
|
762
|
-
if (!baseBranch) {
|
|
763
|
-
throw gitError("missing_base_branch", "Base branch is required.");
|
|
764
|
-
}
|
|
765
|
-
if (!(await localBranchExists(cwd, baseBranch))) {
|
|
766
|
-
throw gitError(
|
|
767
|
-
"missing_base_branch",
|
|
768
|
-
`Base branch '${baseBranch}' is not available locally. Create or check out that branch first.`
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
const currentBranch = typeof status.branch === "string" ? status.branch.trim() : "";
|
|
773
|
-
const canCarryLocalChanges = changeScope.dirty && !!currentBranch && currentBranch === baseBranch;
|
|
774
|
-
if (changeScope.dirty && changeTransfer !== "none" && !canCarryLocalChanges) {
|
|
775
|
-
const currentBranchLabel = currentBranch || "the current branch";
|
|
776
|
-
const transferVerb = changeTransfer === "copy" ? "copy" : "move";
|
|
777
|
-
throw gitError(
|
|
778
|
-
"dirty_worktree_base_mismatch",
|
|
779
|
-
`Uncommitted changes can ${transferVerb} into a managed worktree only from ${currentBranchLabel}. Switch the base branch to match or clean up local changes first.`
|
|
780
|
-
);
|
|
747
|
+
async function gitCreateWorktree(cwd, params) {
|
|
748
|
+
const branch = normalizeCreatedBranchName(params.name);
|
|
749
|
+
if (!branch) {
|
|
750
|
+
throw gitError("missing_branch_name", "Branch name is required.");
|
|
781
751
|
}
|
|
752
|
+
await assertValidCreatedBranchName(cwd, branch);
|
|
782
753
|
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
let copiedLocalChangesPatch = "";
|
|
786
|
-
let didCreateWorktree = false;
|
|
754
|
+
const context = await resolveWorktreeCreationContext(cwd, params, "a new worktree");
|
|
755
|
+
const { branchResult, repoRoot, projectRelativePath } = context;
|
|
787
756
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
757
|
+
const existingWorktreePath = branchResult.worktreePathByBranch[branch];
|
|
758
|
+
if (existingWorktreePath) {
|
|
759
|
+
if (sameFilePath(existingWorktreePath, cwd)) {
|
|
760
|
+
throw gitError(
|
|
761
|
+
"branch_already_open_here",
|
|
762
|
+
`Branch '${branch}' is already open in this project.`
|
|
763
|
+
);
|
|
795
764
|
}
|
|
796
765
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
if (
|
|
801
|
-
await
|
|
802
|
-
}
|
|
803
|
-
if (copiedLocalChangesPatch) {
|
|
804
|
-
await applyCopiedLocalChangesToWorktree(worktreeRootPath, copiedLocalChangesPatch);
|
|
805
|
-
}
|
|
806
|
-
} catch (err) {
|
|
807
|
-
if (didCreateWorktree) {
|
|
808
|
-
await cleanupManagedWorktree(repoRoot, worktreeRootPath);
|
|
809
|
-
} else {
|
|
810
|
-
fs.rmSync(path.dirname(worktreeRootPath), { recursive: true, force: true });
|
|
766
|
+
// Backfill manifest files a worktree created before `.worktreeinclude`
|
|
767
|
+
// existed never received; the copy skips files the worktree already has.
|
|
768
|
+
const existingWorktreeRoot = await resolveRepoRoot(existingWorktreePath).catch(() => null);
|
|
769
|
+
if (existingWorktreeRoot) {
|
|
770
|
+
await copyWorktreeIncludeFiles(repoRoot, existingWorktreeRoot);
|
|
811
771
|
}
|
|
812
772
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
773
|
+
return {
|
|
774
|
+
branch,
|
|
775
|
+
worktreePath: existingWorktreePath,
|
|
776
|
+
alreadyExisted: true,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
816
779
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
780
|
+
if (await localBranchExists(cwd, branch)) {
|
|
781
|
+
throw gitError(
|
|
782
|
+
"branch_exists",
|
|
783
|
+
`Branch '${branch}' already exists locally. Choose another name or open that branch instead.`
|
|
784
|
+
);
|
|
821
785
|
}
|
|
822
786
|
|
|
823
|
-
const
|
|
787
|
+
const { worktreeRootPath } = await createWorktreeAtManagedPath(context, {
|
|
788
|
+
branch,
|
|
789
|
+
failureMessage: "Failed to create worktree.",
|
|
790
|
+
});
|
|
791
|
+
|
|
824
792
|
return {
|
|
825
|
-
|
|
793
|
+
branch,
|
|
794
|
+
worktreePath: scopedWorktreePath(worktreeRootPath, projectRelativePath),
|
|
826
795
|
alreadyExisted: false,
|
|
827
|
-
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
async function gitCreateManagedWorktree(cwd, params) {
|
|
800
|
+
const context = await resolveWorktreeCreationContext(cwd, params, "a managed worktree");
|
|
801
|
+
const { worktreeRootPath, transferredChanges } = await createWorktreeAtManagedPath(context, {
|
|
802
|
+
failureMessage: "Failed to create managed worktree.",
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
return {
|
|
806
|
+
worktreePath: scopedWorktreePath(worktreeRootPath, context.projectRelativePath),
|
|
807
|
+
alreadyExisted: false,
|
|
808
|
+
baseBranch: context.baseBranch,
|
|
828
809
|
headMode: "detached",
|
|
829
|
-
transferredChanges
|
|
810
|
+
transferredChanges,
|
|
830
811
|
};
|
|
831
812
|
}
|
|
832
813
|
|
|
@@ -2005,6 +1986,106 @@ async function rollbackFailedHandoffTransfer(cwd, pathspecArgs = []) {
|
|
|
2005
1986
|
}
|
|
2006
1987
|
}
|
|
2007
1988
|
|
|
1989
|
+
// Mirrors Codex Desktop's `.worktreeinclude`: files Git leaves behind (like an
|
|
1990
|
+
// ignored `.env`) listed in a repository-root manifest are copied into worktrees.
|
|
1991
|
+
// Best effort by design — a stale manifest entry must never fail creation.
|
|
1992
|
+
const WORKTREE_INCLUDE_FILE = ".worktreeinclude";
|
|
1993
|
+
const WORKTREE_INCLUDE_MAX_FILES = 512;
|
|
1994
|
+
|
|
1995
|
+
async function copyWorktreeIncludeFiles(repoRoot, worktreeRootPath) {
|
|
1996
|
+
const manifestPath = path.join(repoRoot, WORKTREE_INCLUDE_FILE);
|
|
1997
|
+
if (!fs.existsSync(manifestPath)) {
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
const relativePaths = await listWorktreeIncludePaths(repoRoot, manifestPath);
|
|
2002
|
+
if (relativePaths.length === 0) {
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
const canonicalWorktreeRoot = await fs.promises.realpath(worktreeRootPath).catch(() => null);
|
|
2007
|
+
if (!canonicalWorktreeRoot) {
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
for (const relativePath of relativePaths) {
|
|
2012
|
+
const sourcePath = path.resolve(repoRoot, relativePath);
|
|
2013
|
+
const destinationPath = path.resolve(worktreeRootPath, relativePath);
|
|
2014
|
+
// `ls-files` output is repo-relative, but keep both sides pinned to their
|
|
2015
|
+
// roots so a hostile manifest entry cannot escape either tree.
|
|
2016
|
+
if (!isPathContainedIn(sourcePath, repoRoot) || !isPathContainedIn(destinationPath, worktreeRootPath)) {
|
|
2017
|
+
continue;
|
|
2018
|
+
}
|
|
2019
|
+
await copyWorktreeIncludeEntry(sourcePath, destinationPath, canonicalWorktreeRoot);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
// Resolves the manifest to repo-relative untracked paths. The manifest uses
|
|
2024
|
+
// gitignore syntax, so let git itself do the matching: `--others -i
|
|
2025
|
+
// --exclude-from` lists every untracked file (ignored or not) that the
|
|
2026
|
+
// manifest patterns select, with full gitignore semantics — nested `.env`
|
|
2027
|
+
// matches, anchored `/foo` and `!negations` behave as documented, and a
|
|
2028
|
+
// malformed line cannot abort the listing the way a bad pathspec would.
|
|
2029
|
+
async function listWorktreeIncludePaths(repoRoot, manifestPath) {
|
|
2030
|
+
let listing = "";
|
|
2031
|
+
try {
|
|
2032
|
+
listing = await git(
|
|
2033
|
+
repoRoot,
|
|
2034
|
+
"ls-files",
|
|
2035
|
+
"--others",
|
|
2036
|
+
"-i",
|
|
2037
|
+
"-z",
|
|
2038
|
+
`--exclude-from=${manifestPath}`
|
|
2039
|
+
);
|
|
2040
|
+
} catch (err) {
|
|
2041
|
+
console.error(`[remodex] .worktreeinclude listing failed: ${err.message}`);
|
|
2042
|
+
return [];
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
const relativePaths = listing.split("\0").filter(Boolean);
|
|
2046
|
+
if (relativePaths.length > WORKTREE_INCLUDE_MAX_FILES) {
|
|
2047
|
+
console.error(
|
|
2048
|
+
`[remodex] .worktreeinclude matched ${relativePaths.length} files; copying only the first ${WORKTREE_INCLUDE_MAX_FILES}. Narrow the manifest patterns.`
|
|
2049
|
+
);
|
|
2050
|
+
relativePaths.length = WORKTREE_INCLUDE_MAX_FILES;
|
|
2051
|
+
}
|
|
2052
|
+
return relativePaths;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
// Copies one manifest match into the worktree, skipping anything unsafe or
|
|
2056
|
+
// already present rather than failing the surrounding creation flow.
|
|
2057
|
+
async function copyWorktreeIncludeEntry(sourcePath, destinationPath, canonicalWorktreeRoot) {
|
|
2058
|
+
try {
|
|
2059
|
+
// Regular files only: copying through a symlinked source would smuggle
|
|
2060
|
+
// out-of-repo content (e.g. `.env -> ~/.ssh/key`) into every worktree.
|
|
2061
|
+
const sourceStats = await fs.promises.lstat(sourcePath);
|
|
2062
|
+
if (!sourceStats.isFile()) {
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
const destinationDirectory = path.dirname(destinationPath);
|
|
2066
|
+
await fs.promises.mkdir(destinationDirectory, { recursive: true });
|
|
2067
|
+
// Re-check containment on the real path: a checked-out symlinked parent
|
|
2068
|
+
// directory would otherwise let the copy write outside the worktree.
|
|
2069
|
+
const canonicalDestinationDirectory = await fs.promises.realpath(destinationDirectory);
|
|
2070
|
+
if (!isPathContainedIn(canonicalDestinationDirectory, canonicalWorktreeRoot)) {
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
// COPYFILE_EXCL keeps existing files intact: change transfer may already
|
|
2074
|
+
// have carried a dirty copy, and reused worktrees keep their own state.
|
|
2075
|
+
await fs.promises.copyFile(
|
|
2076
|
+
sourcePath,
|
|
2077
|
+
path.join(canonicalDestinationDirectory, path.basename(destinationPath)),
|
|
2078
|
+
fs.constants.COPYFILE_EXCL
|
|
2079
|
+
);
|
|
2080
|
+
} catch {
|
|
2081
|
+
// Skip unreadable or already-present entries; the worktree stays usable.
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
function isPathContainedIn(candidatePath, rootPath) {
|
|
2086
|
+
return candidatePath === rootPath || candidatePath.startsWith(rootPath + path.sep);
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2008
2089
|
async function cleanupManagedWorktree(repoRoot, worktreeRootPath, branchName = null) {
|
|
2009
2090
|
try {
|
|
2010
2091
|
await git(repoRoot, "worktree", "remove", "--force", worktreeRootPath);
|
package/src/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
runMacOSBridgeService,
|
|
19
19
|
startMacOSBridgeService,
|
|
20
20
|
stopMacOSBridgeService,
|
|
21
|
+
uninstallMacOSBridgeService,
|
|
21
22
|
} = require("./macos-launch-agent");
|
|
22
23
|
|
|
23
24
|
module.exports = {
|
|
@@ -32,6 +33,7 @@ module.exports = {
|
|
|
32
33
|
runMacOSBridgeService,
|
|
33
34
|
startMacOSBridgeService,
|
|
34
35
|
stopMacOSBridgeService,
|
|
36
|
+
uninstallMacOSBridgeService,
|
|
35
37
|
resetBridgePairing: resetBridgeTrustState,
|
|
36
38
|
openLastActiveThread,
|
|
37
39
|
watchThreadRollout,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// FILE: macos-launch-agent.js
|
|
2
|
-
// Purpose: Owns macOS-only launchd install/start/stop/status helpers for the background Remodex bridge.
|
|
2
|
+
// Purpose: Owns macOS-only launchd install/start/stop/uninstall/status helpers for the background Remodex bridge.
|
|
3
3
|
// Layer: CLI helper
|
|
4
|
-
// Exports: start/stop/status helpers plus the launchd service runner used by `remodex up`.
|
|
4
|
+
// Exports: start/stop/uninstall/status helpers plus the launchd service runner used by `remodex up`.
|
|
5
5
|
// Depends on: child_process, fs, os, path, ./bridge, ./daemon-state, ./codex-desktop-refresher, ./qr, ./secure-device-state
|
|
6
6
|
|
|
7
7
|
const { execFileSync } = require("child_process");
|
|
@@ -33,6 +33,23 @@ const SERVICE_LABEL = "com.remodex.bridge";
|
|
|
33
33
|
const DEFAULT_PAIRING_WAIT_TIMEOUT_MS = 10_000;
|
|
34
34
|
const DEFAULT_PAIRING_WAIT_INTERVAL_MS = 200;
|
|
35
35
|
|
|
36
|
+
// If the saved Node binary or CLI entrypoint disappears (npm uninstall, deleted
|
|
37
|
+
// checkout), exit 0 so launchd's KeepAlive.SuccessfulExit=false stops rescheduling
|
|
38
|
+
// the job; `exec` keeps genuine daemon failures non-zero so they still restart.
|
|
39
|
+
const LAUNCH_AGENT_GUARD_SCRIPT = 'if [ ! -x "$1" ] || [ ! -f "$2" ]; then exit 0; fi; exec "$1" "$2" run-service';
|
|
40
|
+
|
|
41
|
+
// Keeps the guard script constant: the installed paths are shell positionals, never interpolated source.
|
|
42
|
+
function buildLaunchAgentProgramArguments({ nodePath, cliPath }) {
|
|
43
|
+
return [
|
|
44
|
+
"/bin/sh",
|
|
45
|
+
"-c",
|
|
46
|
+
LAUNCH_AGENT_GUARD_SCRIPT,
|
|
47
|
+
SERVICE_LABEL,
|
|
48
|
+
nodePath,
|
|
49
|
+
cliPath,
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
// Runs the bridge inside launchd while keeping QR rendering in the foreground CLI command.
|
|
37
54
|
function runMacOSBridgeService({ env = process.env, platform = process.platform } = {}) {
|
|
38
55
|
assertDarwinPlatform(platform);
|
|
@@ -123,17 +140,19 @@ async function startMacOSBridgeService({
|
|
|
123
140
|
};
|
|
124
141
|
}
|
|
125
142
|
|
|
126
|
-
// Restarts the installed LaunchAgent without rewriting relay config,
|
|
143
|
+
// Restarts the installed LaunchAgent without rewriting relay config, regenerating the plist so
|
|
144
|
+
// legacy launch definitions pick up the current Node/CLI paths and launch policy.
|
|
127
145
|
async function restartMacOSBridgeService({
|
|
128
146
|
env = process.env,
|
|
129
147
|
platform = process.platform,
|
|
130
148
|
fsImpl = fs,
|
|
131
149
|
execFileSyncImpl = execFileSync,
|
|
132
150
|
osImpl = os,
|
|
151
|
+
nodePath = process.execPath,
|
|
152
|
+
cliPath = path.resolve(__dirname, "..", "bin", "remodex.js"),
|
|
133
153
|
waitForPairing = false,
|
|
134
154
|
pairingTimeoutMs = DEFAULT_PAIRING_WAIT_TIMEOUT_MS,
|
|
135
155
|
pairingPollIntervalMs = DEFAULT_PAIRING_WAIT_INTERVAL_MS,
|
|
136
|
-
...startOptions
|
|
137
156
|
} = {}) {
|
|
138
157
|
assertDarwinPlatform(platform);
|
|
139
158
|
const plistPath = resolveLaunchAgentPlistPath({ env, osImpl });
|
|
@@ -144,10 +163,11 @@ async function restartMacOSBridgeService({
|
|
|
144
163
|
fsImpl,
|
|
145
164
|
execFileSyncImpl,
|
|
146
165
|
osImpl,
|
|
166
|
+
nodePath,
|
|
167
|
+
cliPath,
|
|
147
168
|
waitForPairing,
|
|
148
169
|
pairingTimeoutMs,
|
|
149
170
|
pairingPollIntervalMs,
|
|
150
|
-
...startOptions,
|
|
151
171
|
});
|
|
152
172
|
}
|
|
153
173
|
|
|
@@ -156,7 +176,16 @@ async function restartMacOSBridgeService({
|
|
|
156
176
|
clearPairingSession({ env, fsImpl });
|
|
157
177
|
}
|
|
158
178
|
|
|
159
|
-
|
|
179
|
+
ensureRemodexStateDir({ env, fsImpl, osImpl });
|
|
180
|
+
ensureRemodexLogsDir({ env, fsImpl, osImpl });
|
|
181
|
+
writeLaunchAgentPlist({
|
|
182
|
+
env,
|
|
183
|
+
fsImpl,
|
|
184
|
+
osImpl,
|
|
185
|
+
nodePath,
|
|
186
|
+
cliPath,
|
|
187
|
+
});
|
|
188
|
+
restartLaunchAgent({
|
|
160
189
|
env,
|
|
161
190
|
execFileSyncImpl,
|
|
162
191
|
plistPath,
|
|
@@ -204,6 +233,31 @@ function stopMacOSBridgeService({
|
|
|
204
233
|
clearBridgeStatus({ env, fsImpl });
|
|
205
234
|
}
|
|
206
235
|
|
|
236
|
+
// Removes launchd ownership of the bridge (unload + plist) while preserving daemon config,
|
|
237
|
+
// logs, device trust, and pairing identity for a future reinstall.
|
|
238
|
+
function uninstallMacOSBridgeService({
|
|
239
|
+
env = process.env,
|
|
240
|
+
platform = process.platform,
|
|
241
|
+
execFileSyncImpl = execFileSync,
|
|
242
|
+
fsImpl = fs,
|
|
243
|
+
osImpl = os,
|
|
244
|
+
processImpl = process,
|
|
245
|
+
} = {}) {
|
|
246
|
+
assertDarwinPlatform(platform);
|
|
247
|
+
const plistPath = resolveLaunchAgentPlistPath({ env, osImpl });
|
|
248
|
+
const removed = fsImpl.existsSync(plistPath);
|
|
249
|
+
// Stop first: a real bootout failure throws here and leaves the plist on disk.
|
|
250
|
+
stopMacOSBridgeService({
|
|
251
|
+
env,
|
|
252
|
+
platform,
|
|
253
|
+
execFileSyncImpl,
|
|
254
|
+
fsImpl,
|
|
255
|
+
processImpl,
|
|
256
|
+
});
|
|
257
|
+
fsImpl.rmSync(plistPath, { force: true });
|
|
258
|
+
return { plistPath, removed };
|
|
259
|
+
}
|
|
260
|
+
|
|
207
261
|
// Revokes pairing immediately on macOS by stopping the daemon before rotating identity/trust state.
|
|
208
262
|
function resetMacOSBridgePairing({
|
|
209
263
|
env = process.env,
|
|
@@ -374,9 +428,9 @@ function buildLaunchAgentPlist({
|
|
|
374
428
|
<string>${escapeXml(SERVICE_LABEL)}</string>
|
|
375
429
|
<key>ProgramArguments</key>
|
|
376
430
|
<array>
|
|
377
|
-
|
|
378
|
-
<string>${escapeXml(
|
|
379
|
-
|
|
431
|
+
${buildLaunchAgentProgramArguments({ nodePath, cliPath })
|
|
432
|
+
.map((argument) => ` <string>${escapeXml(argument)}</string>`)
|
|
433
|
+
.join("\n")}
|
|
380
434
|
</array>
|
|
381
435
|
<key>RunAtLoad</key>
|
|
382
436
|
<true/>
|
|
@@ -455,31 +509,6 @@ function restartLaunchAgent({
|
|
|
455
509
|
], { stdio: ["ignore", "ignore", "pipe"] });
|
|
456
510
|
}
|
|
457
511
|
|
|
458
|
-
function kickstartLaunchAgent({
|
|
459
|
-
env = process.env,
|
|
460
|
-
execFileSyncImpl = execFileSync,
|
|
461
|
-
plistPath,
|
|
462
|
-
} = {}) {
|
|
463
|
-
try {
|
|
464
|
-
execFileSyncImpl("launchctl", [
|
|
465
|
-
"kickstart",
|
|
466
|
-
"-k",
|
|
467
|
-
launchAgentLabelDomain(env),
|
|
468
|
-
], { stdio: ["ignore", "ignore", "pipe"] });
|
|
469
|
-
} catch {
|
|
470
|
-
execFileSyncImpl("launchctl", [
|
|
471
|
-
"bootstrap",
|
|
472
|
-
launchAgentDomain(env),
|
|
473
|
-
plistPath,
|
|
474
|
-
], { stdio: ["ignore", "ignore", "pipe"] });
|
|
475
|
-
execFileSyncImpl("launchctl", [
|
|
476
|
-
"kickstart",
|
|
477
|
-
"-k",
|
|
478
|
-
launchAgentLabelDomain(env),
|
|
479
|
-
], { stdio: ["ignore", "ignore", "pipe"] });
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
512
|
function bootoutLaunchAgent({
|
|
484
513
|
env = process.env,
|
|
485
514
|
execFileSyncImpl = execFileSync,
|
|
@@ -675,6 +704,7 @@ function shortFingerprint(value) {
|
|
|
675
704
|
module.exports = {
|
|
676
705
|
buildTrustedDeviceSummary,
|
|
677
706
|
buildLaunchAgentPlist,
|
|
707
|
+
buildLaunchAgentProgramArguments,
|
|
678
708
|
getMacOSBridgeServiceStatus,
|
|
679
709
|
mergeBridgeStatusForDaemon,
|
|
680
710
|
printMacOSBridgePairingQr,
|
|
@@ -685,4 +715,5 @@ module.exports = {
|
|
|
685
715
|
runMacOSBridgeService,
|
|
686
716
|
startMacOSBridgeService,
|
|
687
717
|
stopMacOSBridgeService,
|
|
718
|
+
uninstallMacOSBridgeService,
|
|
688
719
|
};
|