Muxy

Git

muxy.git gives extensions full programmatic access to the repository behind the active project — status, diffs, history, branches, pull requests, and worktrees. It is the same git core the app and the mobile remote use, so there is one source of truth for everything git.

On tabs/panels/popovers these methods return a Promise (use await); in runScript commands and background scripts the same calls are synchronous and return the value directly. They operate on the active worktree of a project. Pass { project } (a project id, name, or path) to target a specific project; omit it to use the active one.

Use muxy.git instead of shelling out with muxy.exec — it returns structured data, is cached, and avoids spawning a git/gh process per call.

Caching

Read methods are cached per project/worktree. A cached value is reused only while the repository's HEAD and index are unchanged, so a commit, stage, or checkout — from the extension or an external terminal — invalidates it automatically. There is also a short time bound so network-backed pr.* calls don't run on every request.

Pass { fresh: true } to any read method to bypass the cache and force a refresh:

await muxy.git.status();              // cached
await muxy.git.status({ fresh: true }); // always recomputed

Permissions

PermissionMethods
git:readstatus, diff, repoInfo, log, branches, remoteBranches, currentBranch, aheadBehind, pr.info, pr.number, pr.diff, pr.list, worktrees
git:writeinit, stage, unstage, discard, commit, push, pull, checkout, cherryPick, revert, branch.create, branch.switchTo, branch.delete, branch.deleteRemote, tag.create, pr.create, pr.merge, pr.close, pr.checkout, pr.checkoutWorktree, worktree.add, worktree.remove, worktree.switchTo

Every write also prompts the user for runtime consent the first time, remembered as an allow/deny rule for the extension.

{
  "name": "git-tools",
  "version": "0.1.0",
  "permissions": ["git:read", "git:write"]
}

Read methods

muxy.git.status(opts?)

Returns a snapshot of the working tree:

const s = await muxy.git.status();
// {
//   branch: "feature/x",
//   aheadBehind: { ahead: 2, behind: 0, hasUpstream: true },
//   defaultBranch: "main",
//   branches: ["main", "feature/x"],
//   stagedFiles:   [{ path, oldPath, status, isStaged, isUnstaged, isBinary, additions, deletions }],
//   unstagedFiles: [ ... ],
//   pullRequest: null | { url, number, state, isDraft, baseBranch, mergeable, mergeStateStatus, isCrossRepository, checks }
// }

status on each file is the git status letter (M, A, D, R, ?, …).

Pass { local: true } to skip pull-request enrichment — it avoids the gh network call and returns pullRequest: null, which is faster when you only need the working tree.

muxy.git.repoInfo(opts?)

await muxy.git.repoInfo();
// { root, gitDir, isWorktree, currentBranch }

Resolves the repository layout in a single call — root (top level), gitDir, whether the active checkout is a linked isWorktree, and the currentBranch.

muxy.git.diff(opts)

const d = await muxy.git.diff({ filePath: "src/main.swift", staged: false });
// { additions, deletions, truncated, rows: [{ kind, oldLineNumber, newLineNumber, oldText, newText, text }] }

const raw = await muxy.git.diff({ raw: true });
// { diff: "<unified diff text>", truncated }
  • filePath — path relative to the repo root. Required for the parsed mode; optional in raw mode (omit for the whole repo).
  • rawtrue returns the raw unified diff string instead of parsed rows.
  • stagedtrue for the staged diff, false/omitted for the working-tree diff.
  • lineLimit — cap the number of lines (omit for full).

kind is one of hunk, context, addition, deletion, collapsed.

muxy.git.log(opts?)

const commits = await muxy.git.log({ maxCount: 50, skip: 0 });
// [{ hash, shortHash, subject, authorName, authorDate, isMerge, parentHashes, refs: [{ name, kind }] }]

maxCount defaults to 100 and skip to 0 when omitted.

muxy.git.branches(opts?) · muxy.git.remoteBranches(opts?) · muxy.git.currentBranch(opts?) · muxy.git.aheadBehind(opts?)

await muxy.git.branches();        // ["main", "feature/x"]
await muxy.git.remoteBranches();  // ["origin/main", "origin/feature/x"]
await muxy.git.currentBranch();   // "feature/x"
await muxy.git.aheadBehind();     // { ahead, behind, hasUpstream }

muxy.git.pr.info(opts?) · muxy.git.pr.number(opts?) · muxy.git.pr.diff(opts) · muxy.git.pr.list(opts?)

await muxy.git.pr.info();                                  // PR for the current branch, or null
await muxy.git.pr.number();                                // just the PR number for the current branch, or null
await muxy.git.pr.diff({ number: 42 });                    // { diff: "<unified diff text>", truncated }
await muxy.git.pr.list({ filter: "open", limit: 50 });     // filter: open | closed | merged | all
await muxy.git.pr.list({ checks: false });                 // skip CI checks for a lighter, faster query

pr.number is a cheap way to learn whether the current branch has a PR without fetching its full status. pr.diff fetches the PR head and returns the raw diff against its merge base. pr.list defaults to filter: "open" and limit: 100, and includes each PR's CI checks by default; pass checks: false to drop the statusCheckRollup field, which makes the underlying query much lighter and avoids GitHub timeouts (504) on large repositories. All require the GitHub CLI (gh) to be installed and authenticated.

muxy.git.worktrees(opts?)

await muxy.git.worktrees();
// [{ path, branch, head, isBare, isDetached, isPrunable }]

Write methods

All writes prompt for consent on first use.

await muxy.git.init();                              // git init in the active project folder

await muxy.git.stage({ paths: ["a.txt"] });        // empty paths => stage all
await muxy.git.unstage({ paths: ["a.txt"] });      // empty paths => unstage all
await muxy.git.discard({ paths: [], untrackedPaths: ["tmp.log"] });

await muxy.git.commit({ message: "Fix bug", stageAll: true }); // => { hash }
await muxy.git.push();                      // sets upstream automatically if missing
await muxy.git.push({ setUpstream: true }); // always push --set-upstream origin <branch>
await muxy.git.pull();

await muxy.git.checkout({ hash: "a1b2c3d" });          // detached checkout of a commit
await muxy.git.cherryPick({ hash: "a1b2c3d" });
await muxy.git.revert({ hash: "a1b2c3d" });            // staged, not committed

await muxy.git.branch.create({ name: "feature/y" });   // creates and switches
await muxy.git.branch.switchTo({ branch: "main" });
await muxy.git.branch.delete({ name: "feature/old", force: false }); // local delete (-d, or -D when force)
await muxy.git.branch.deleteRemote({ branch: "feature/old" });

await muxy.git.tag.create({ name: "v1.0.0", hash: "a1b2c3d" });

await muxy.git.pr.create({ title: "Add Y", body: "…", baseBranch: "main", draft: false }); // => PR info
await muxy.git.pr.merge({ number: 42, method: "squash", deleteBranch: true }); // method: merge | squash | rebase
await muxy.git.pr.close({ number: 42 });
await muxy.git.pr.checkout({ number: 42 });                              // checks the PR out locally
await muxy.git.pr.checkoutWorktree({ number: 42, path: "~/code/pr-42" }); // => { branch }

await muxy.git.worktree.add({ path: "~/code/app-y", branch: "feature/y", createBranch: true, baseBranch: "main" }); // => the resolved worktree path
const removed = await muxy.git.worktree.remove({
  path: "~/code/app-y",
  force: false,
  timeoutMs: 30000,
}); // => { path, dirRemoved: true | false | null }
await muxy.git.worktree.switchTo({ identifier: "feature/y" }); // activate a worktree (id, name, branch, or path)

Errors

A rejected promise carries a message string:

  • permission denied (git:read|git:write) — missing manifest permission.
  • user denied consent for git.<op> — the write consent prompt was denied.
  • project not found … — the project selector did not resolve.
  • invalid arguments … — a required field was missing.
  • Git timed out after <n>s, Process timed out after <n>s, or Operation timed out after <n>stimeoutMs expired during removal.
  • Anything else surfaces the underlying git/gh error text.
try {
  await muxy.git.commit({ message: "" });
} catch (err) {
  console.error(err.message); // "commit message is required"
}

Notes

  • muxy.git is available to extension tabs, panels, popovers, runScript commands, and background scripts — the same API and permissions everywhere. Calls return a Promise on webview pages and are synchronous in runScript and background scripts.
  • The app continues to own the worktree lifecycle it shows in the sidebar; git.worktree.* operates on the same underlying git worktrees, so changes are reflected after a refresh.
  • worktree.remove deregisters the worktree while retaining its branch and updates tracked sidebar state for the selected project. Muxy-managed local worktrees run configured teardown hooks first; remote and externally managed worktrees do not run local teardown hooks. If a tracked local worktree's main repository is missing, Muxy removes the tracked sidebar entry but preserves all files at the worktree path and returns dirRemoved: false, regardless of force. Otherwise, with force: false, Git performs the final dirty-worktree check and rejects removal when changes remain; pass force: true to discard them. timeoutMs defaults to 30 seconds, accepts up to 3,600,000 ms (one hour), covers path resolution, teardown, Git removal, and final verification, and terminates the active process group when exceeded. In the result, dirRemoved is true when the path is definitely gone, false when files remain, and null when the final existence check could not be completed within the deadline.
  • There are no AI helpers here — generate commit messages or PR bodies with your own model via muxy.exec if you need them.