@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.
@@ -635,13 +635,11 @@ async function gitCreateBranch(cwd, params) {
635
635
  return { branch: name, status };
636
636
  }
637
637
 
638
- async function gitCreateWorktree(cwd, params) {
639
- const branch = normalizeCreatedBranchName(params.name);
640
- if (!branch) {
641
- throw gitError("missing_branch_name", "Branch name is required.");
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 a new worktree only from ${currentBranchLabel}. Switch the base branch to match or clean up local changes first.`
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
- const existingWorktreePath = branchResult.worktreePathByBranch[branch];
674
- if (existingWorktreePath) {
675
- if (sameFilePath(existingWorktreePath, cwd)) {
676
- throw gitError(
677
- "branch_already_open_here",
678
- `Branch '${branch}' is already open in this project.`
679
- );
680
- }
681
-
682
- return {
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
- await git(repoRoot, "worktree", "add", "-b", branch, worktreeRootPath, baseBranch);
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 (err.message?.includes("already exists")) {
735
- throw gitError("branch_exists", `Branch '${branch}' already exists.`);
736
- }
737
- if (err.message?.includes("already used by worktree") || err.message?.includes("already checked out at")) {
738
- throw gitError(
739
- "branch_in_other_worktree",
740
- `Branch '${branch}' is already open in another worktree.`
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 || "Failed to create worktree.");
738
+ throw gitError("create_worktree_failed", err.message || failureMessage);
744
739
  }
745
740
 
746
- const worktreePath = scopedWorktreePath(worktreeRootPath, projectRelativePath);
747
741
  return {
748
- branch,
749
- worktreePath,
750
- alreadyExisted: false,
742
+ worktreeRootPath,
743
+ transferredChanges: Boolean(handoffStashRef || copiedLocalChangesPatch),
751
744
  };
752
745
  }
753
746
 
754
- async function gitCreateManagedWorktree(cwd, params) {
755
- const branchResult = await gitBranches(cwd);
756
- const repoRoot = await resolveRepoRoot(cwd);
757
- const status = await gitStatus(cwd);
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 worktreeRootPath = allocateManagedWorktreePath(repoRoot);
784
- let handoffStashRef = null;
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
- try {
789
- if (canCarryLocalChanges) {
790
- if (changeTransfer === "copy") {
791
- copiedLocalChangesPatch = await captureLocalChangesPatch(repoRoot, changeScope.pathspecArgs);
792
- } else if (changeTransfer === "move") {
793
- handoffStashRef = await stashChangesForWorktreeHandoff(repoRoot, changeScope.pathspecArgs);
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
- await git(repoRoot, "worktree", "add", "--detach", worktreeRootPath, baseBranch);
798
- didCreateWorktree = true;
799
-
800
- if (handoffStashRef) {
801
- await applyWorktreeHandoffStash(worktreeRootPath, handoffStashRef);
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
- if (handoffStashRef) {
814
- await restoreWorktreeHandoffStash(repoRoot, handoffStashRef);
815
- }
773
+ return {
774
+ branch,
775
+ worktreePath: existingWorktreePath,
776
+ alreadyExisted: true,
777
+ };
778
+ }
816
779
 
817
- if (err.message?.includes("invalid reference")) {
818
- throw gitError("missing_base_branch", `Base branch '${baseBranch}' does not exist.`);
819
- }
820
- throw gitError("create_worktree_failed", err.message || "Failed to create managed worktree.");
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 worktreePath = scopedWorktreePath(worktreeRootPath, projectRelativePath);
787
+ const { worktreeRootPath } = await createWorktreeAtManagedPath(context, {
788
+ branch,
789
+ failureMessage: "Failed to create worktree.",
790
+ });
791
+
824
792
  return {
825
- worktreePath,
793
+ branch,
794
+ worktreePath: scopedWorktreePath(worktreeRootPath, projectRelativePath),
826
795
  alreadyExisted: false,
827
- baseBranch,
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: Boolean(handoffStashRef || copiedLocalChangesPatch),
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, useful during local bridge development.
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
- kickstartLaunchAgent({
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
- <string>${escapeXml(nodePath)}</string>
378
- <string>${escapeXml(cliPath)}</string>
379
- <string>run-service</string>
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
  };