borgmcp 1.0.31 → 1.0.32
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/dist/claude.js +11 -10
- package/dist/cleanup-cmd.d.ts +142 -0
- package/dist/cleanup-cmd.js +13 -0
- package/dist/cli-help.js +1 -0
- package/package.json +1 -1
package/dist/claude.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{spawn as D}from"child_process";import{randomUUID as L}from"node:crypto";import{basename as M}from"node:path";import{createInterface as E}from"node:readline/promises";import
|
|
3
|
-
`)})();if((process.argv[2]==="--help"||process.argv[2]==="-h")&&(process.stdout.write(V(g())),process.exit(0)),process.argv[2]==="setup"){y(process.argv[3])&&(process.stdout.write(U(g())),process.exit(0)),await import("./setup.js");return}if(process.argv[2]==="assimilate"){process.argv.slice(3).some(y)&&(process.stdout.write(j(g())),process.exit(0));const e=
|
|
4
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=
|
|
5
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=await z({},e.options);process.exit(r)}if(process.argv[2]==="
|
|
6
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=
|
|
7
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const
|
|
8
|
-
|
|
9
|
-
`)}`)
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
import{spawn as D}from"child_process";import{randomUUID as L}from"node:crypto";import{basename as M}from"node:path";import{createInterface as E}from"node:readline/promises";import t from"chalk";import{findProjectRoot as O,getActiveCube as H,getLaunchModel as F,inboxPathForDrone as N,setCodexWakeTarget as B,pruneDeadCodexWakeTargets as _}from"./cubes.js";import{applyOllamaLaunchEnv as G,checkModelReachable as W}from"./model-presets.js";import{handleVersionFlag as Y,getPackageVersion as g}from"./version.js";import{isHelpFlag as y,setupHelpText as U,topLevelHelpText as V,assimilateHelpText as j}from"./cli-help.js";import{runSpawn as K}from"./spawn.js";import{parseSyncArgs as q,runSync as z}from"./sync.js";import{parseCleanupArgs as X,runCleanup as J}from"./cleanup-cmd.js";import{parseAssimilateArgs as Q}from"./parse-assimilate-args.js";import{runAssimilate as Z}from"./assimilate-cmd.js";import{buildDefaultAssimilateDeps as ee}from"./assimilate-deps.js";import{parseLaunchAllArgs as S}from"./parse-launch-all-args.js";import{runLaunchAll as T}from"./launch-all-cmd.js";import{buildDefaultLaunchAllDeps as w}from"./launch-all-deps.js";import{discoverDroneCandidates as re}from"./launch-all-discovery.js";import{runBareLaunchMenu as oe,shouldShowLaunchMenu as se}from"./bare-launch-menu.js";import{setTerminalTitle as te}from"./terminal-title.js";import{initConsolePrefix as ae,consolePrefix as o}from"./console-prefix.js";import{initDebugFromArgv as ie}from"./debug.js";import{fetchLatestBorgmcpVersion as ne,compareVersionsForStaleness as ce}from"./stale-version-check.js";import{defaultCliChoiceDeps as le,detectCliAvailability as x,installedCliNames as A,parseCliFlag as de,resolveCliChoice as pe}from"./cli-platform.js";import{getRefreshToken as ue,getIdToken as me}from"./config.js";import{composeGetStarted as fe,shouldShowGetStarted as ge}from"./get-started.js";import{prepareCodexRemoteLaunch as he,withCodexCwdArg as we,defaultCodexRemoteDeps as xe,checkCodexBridgeHealthy as ve}from"./codex-remote.js";import{findLoadedCodexThread as be}from"./codex-app-server.js";import{buildAgentKickoffPrompt as Ce,recordCodexWakeTarget as ke,socketPathFromRemoteArgs as I}from"./codex-launch.js";import{codexBorgSessionConfigArgs as $e}from"./launch-gate.js";import{addCodexMcpServer as ye,addCodexSessionStartHook as Se,addCodexUserPromptSubmitHook as Te,addMcpServer as Ae,addProjectSessionStartHook as Ie,addUserPromptSubmitHook as Pe,isCodexMcpServerConfigured as Re,isMcpServerConfigured as De,removeSessionStartHook as Le}from"./config-utils.js";async function Me(){ie(process.argv),Y(),await ae();const c=(async()=>{if(!process.stderr.isTTY)return;const e=g(),r=await ne();if(!r)return;const i=ce(e,r);i.stale&&i.message&&process.stderr.write(`${o()}${i.message}
|
|
3
|
+
`)})();if((process.argv[2]==="--help"||process.argv[2]==="-h")&&(process.stdout.write(V(g())),process.exit(0)),process.argv[2]==="setup"){y(process.argv[3])&&(process.stdout.write(U(g())),process.exit(0)),await import("./setup.js");return}if(process.argv[2]==="assimilate"){process.argv.slice(3).some(y)&&(process.stdout.write(j(g())),process.exit(0));const e=Q(process.argv.slice(3));e.ok||(process.stderr.write(t.red(`${o()}\u25FC borg assimilate: ${e.error}
|
|
4
|
+
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=ee(),i=await Z({role:e.role,flags:e.flags},r);process.exit(i)}if(process.argv[2]==="spawn"){const e=await K();process.exit(e)}if(process.argv[2]==="sync"){const e=q(process.argv.slice(3));e.ok||(process.stderr.write(t.red(`${o()}\u25FC borg sync: ${e.error}
|
|
5
|
+
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=await z({},e.options);process.exit(r)}if(process.argv[2]==="cleanup"){const e=X(process.argv.slice(3));e.ok||(process.stderr.write(t.red(`${o()}\u25FC borg cleanup: ${e.error}
|
|
6
|
+
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=await J({},e.options);process.exit(r)}if(process.argv[2]==="launch-all"){const e=S(process.argv.slice(3));e.ok||(process.stderr.write(t.red(`${o()}\u25FC borg launch-all: ${e.error}
|
|
7
|
+
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const r=w(),i=await T(e.args,r);process.exit(i)}if(ge(await ue()!==null,await me()!==null)){const e=A(x()).length>0;process.stdout.write(fe(e)),process.exit(0)}const n=de(process.argv.slice(2));n.error&&(process.stderr.write(t.red(`${o()}\u25FC ${n.error}
|
|
8
|
+
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const v=async e=>{const r=E({input:process.stdin,output:process.stdout});try{return await r.question(e)}finally{r.close()}};let s=await pe(n.cli,le(v,()=>process.stdin.isTTY===!0));Ee();const a=await H();if(se({extraArgs:process.argv.slice(2),stdinIsTTY:process.stdin.isTTY===!0,stdoutIsTTY:process.stdout.isTTY===!0})){const e=A(x()).find(m=>m!==s)??null;let r=!1;a&&(r=(await re({targetCubeId:a.cubeId},w())).length>0);const i=await oe({defaultCli:s,otherInstalledCli:e,hasLaunchAllTargets:r},v);if(i.kind==="launch-all"){const m=S([]),R=m.ok?await T(m.args,w()):1;process.exit(R)}s=i.cli}const l=n.rest;te(a?{label:a.droneLabel,cubeName:a.name}:null,M(process.cwd()));const P=a&&s==="claude"?`If you haven't yet, arm a persistent Monitor running the command \`borg-inbox-monitor ${N(a.cubeId,a.droneId)}\` so each event's task-notification title summarizes the new cube log entry (drone label, role, and first ~80 chars of the message body) \u2014 letting you triage events without reading the full body. `:"";await Promise.race([c,new Promise(e=>setTimeout(e,2e3))]);const b=s==="codex"?`borg-wake-${L()}`:null;let f,C=[],d={...process.env,BORG_SESSION:"1"},p=null,u=null;if(s==="codex"&&!l.includes("--remote")){console.error(`${o()}${t.gray("\u25FC Starting Codex remote-wake app-server\u2026")}`);const e=await he(xe());e.warning?(console.error(`${o()}${t.yellow(`warning: ${e.warning}`)}`),f="\u26A0 Codex wake-path capability check failed: remote-control is unavailable for this session. Run borg_regen manually whenever you return, and expect only fallback wakeups until relaunch."):f="Codex wake-path capability check passed: remote-control socket established for this session.",C=e.args,d={...process.env,...e.env,BORG_SESSION:"1"},p=I(e.args),u=e.server?.cleanup??null}else s==="codex"&&l.includes("--remote")&&(f="Codex wake-path capability check: using caller-provided --remote socket; if no wake arrives, run borg_regen manually when returning to the session.",p=I(l),p&&(d={...process.env,BORG_CODEX_REMOTE_WAKE:"1",BORG_SESSION:"1"}));if(a){const e=await F(a.cubeId,a.droneId),r=G(d,e,process.env);if(d=r.env,r.probe){const i=await W(r.probe.descriptor,fetch,r.probe.baseUrl);i.ok||console.error(`${o()}${t.yellow(`warning: ${i.message}`)}`)}}const k=Ce({cli:s,codexWakeNonce:b,monitorClause:P,codexWakePathClause:f});let h=[...l,k];s==="codex"&&(h=[...$e(),...C,...we(h,process.cwd())]),console.error(`${o()}${t.blue(`\u25FC Launching ${s==="claude"?"Claude Code":"Codex"}\u2026`)}`);const $=D(s,h,{stdio:"inherit",shell:!1,env:d});s==="codex"&&a&&p&&(ke({deps:{setCodexWakeTarget:B,findLoadedCodexThread:be},cubeId:a.cubeId,droneId:a.droneId,socketPath:p,passthroughArgs:l,previewNeedle:b??k.slice(0,120),cwd:process.cwd(),launchedAtSeconds:Math.floor(Date.now()/1e3)}),_(e=>ve(e))),$.on("error",e=>{if(u)try{u()}catch{}e.code==="ENOENT"?(console.error(`${o()}${t.red(`
|
|
9
|
+
\u25FC Failed to launch ${s}`)}`),console.error(`${o()}${t.gray(`Make sure ${s} is installed.
|
|
10
|
+
`)}`)):console.error(`${o()}${t.red(`
|
|
11
|
+
\u25FC Failed to launch ${s}: ${e.message}
|
|
12
|
+
`)}`),process.exit(1)}),$.on("exit",e=>{if(u)try{u()}catch{}process.exit(e??0)})}function Ee(){const c=x();if(c.claude)try{De()||Ae(),Ie(O(process.cwd())),Le(),Pe()}catch(n){console.error(`${o()}${t.yellow(`warning: Claude Code integration check failed: ${n?.message??n}`)}`)}if(c.codex)try{Re()||ye(),Se(),Te()}catch(n){console.error(`${o()}${t.yellow(`warning: Codex integration check failed: ${n?.message??n}`)}`)}}Me().catch(c=>{console.error(`${o()}${t.red(`
|
|
12
13
|
\u25FC Error: ${c.message}
|
|
13
14
|
`)}`),process.exit(1)});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `borg cleanup` — reap orphaned worktrees for evicted drones (gh#882).
|
|
3
|
+
*
|
|
4
|
+
* An evicted drone (manual `borg_evict-drone`, watchdog
|
|
5
|
+
* `autoEvictPresumedDead`, or gh#877 graceful self-shutdown) leaves its
|
|
6
|
+
* worktree dir (`~/.borg/worktrees/<repo>/<name>`) + its `wt-<suffix>`
|
|
7
|
+
* branch on disk. Nothing reclaims them. This command finds and SAFELY
|
|
8
|
+
* removes worktrees orphaned by eviction — never destroying live, frozen,
|
|
9
|
+
* dirty, unmerged, or precious-local-state work.
|
|
10
|
+
*
|
|
11
|
+
* Pure composition over `worktree-lifecycle.ts` + the gh#877 per-seat
|
|
12
|
+
* `410 DRONE_EVICTED` discrimination (via the seat-probe seam). All git +
|
|
13
|
+
* network + filesystem effects are behind injected deps so every branch is
|
|
14
|
+
* unit-testable without a live repo, network, or $HOME.
|
|
15
|
+
*
|
|
16
|
+
* SEC gate (gh#882, S1–S5 — design entries 98e45fbf / acd76794):
|
|
17
|
+
* S1 Destructive prune authority = server 410 DRONE_EVICTED on the
|
|
18
|
+
* worktree's OWN saved-seat token, ONLY.
|
|
19
|
+
* S2 KEEP classes (any one → never prune, surface): 423 DRONE_FROZEN
|
|
20
|
+
* (reversible); dirty; unmerged; non-regenerable gitignored-local.
|
|
21
|
+
* S3 Report-only until gh#877 is DEPLOYED. The destructive path keys on
|
|
22
|
+
* the 410 CODE — pre-deploy the server returns 401 → probe resolves
|
|
23
|
+
* 'indeterminate' → report-only → safe. No flag / deploy coupling;
|
|
24
|
+
* --prune auto-unlocks the day 410 ships.
|
|
25
|
+
* S4 (a) gitignored-aware clean-gate, DEFAULT-DENY: block on ANY
|
|
26
|
+
* gitignored-local present UNLESS it matches a curated REGENERABLE
|
|
27
|
+
* allowlist (NOT a secret denylist — that fails open). Layered here,
|
|
28
|
+
* NOT in the shared isCleanTree/classifyDirty (those feed
|
|
29
|
+
* assimilate/sync — widening them perturbs that path).
|
|
30
|
+
* (b) fetch origin/main BEFORE the isMerged gate.
|
|
31
|
+
* S5 Auto-prune strictly under canonicalized (realpath) worktreesHome —
|
|
32
|
+
* reject symlink-escape / `..`. Report MAY widen to legacy siblings
|
|
33
|
+
* outside worktreesHome (label "legacy — manual review"); report ≠ delete.
|
|
34
|
+
*/
|
|
35
|
+
import { type RunSync } from './worktree-lifecycle.js';
|
|
36
|
+
import { type ActiveCube } from './cubes.js';
|
|
37
|
+
/**
|
|
38
|
+
* Eviction-probe verdict for ONE worktree's saved seat (gh#877 reuse).
|
|
39
|
+
* Mapped 1:1 from the server's per-caller-seat discrimination:
|
|
40
|
+
* evicted ← 410 DRONE_EVICTED (the ONLY delete authority — S1)
|
|
41
|
+
* frozen ← 423 DRONE_FROZEN (reversible — KEEP)
|
|
42
|
+
* live ← 200 (resolves — KEEP)
|
|
43
|
+
* indeterminate ← 401 / 404 / timeout / 5xx / network (transient or
|
|
44
|
+
* pre-gh#877-deploy) → report-only, NEVER delete (S2/S3)
|
|
45
|
+
*/
|
|
46
|
+
export type SeatStatus = 'evicted' | 'frozen' | 'live' | 'indeterminate';
|
|
47
|
+
/** Per-worktree classification outcome. PRUNABLE is the ONLY delete class. */
|
|
48
|
+
export type CleanupReason = 'PRUNABLE' | 'SURVIVES-dirty' | 'SURVIVES-clobber' | 'SURVIVES-unmerged' | 'SURVIVES-detached' | 'SURVIVES-frozen' | 'SURVIVES-live' | 'SURVIVES-self' | 'UNKNOWN-indeterminate' | 'UNKNOWN-no-seat' | 'LEGACY-manual-review';
|
|
49
|
+
export interface CleanupRow {
|
|
50
|
+
worktreePath: string;
|
|
51
|
+
wtBranch: string | null;
|
|
52
|
+
reason: CleanupReason;
|
|
53
|
+
/** Extra human detail (e.g. the blocking file, or the probe outcome). */
|
|
54
|
+
detail?: string;
|
|
55
|
+
/** Set after a --prune attempt on a PRUNABLE row. */
|
|
56
|
+
prune?: 'removed' | 'remove-failed' | 'branch-delete-failed';
|
|
57
|
+
}
|
|
58
|
+
export interface CleanupDeps {
|
|
59
|
+
/** Synchronous subprocess runner — `git ...`. */
|
|
60
|
+
runSync?: RunSync;
|
|
61
|
+
/** Resolved $HOME (worktreesHome anchor). Injected for tests. */
|
|
62
|
+
homeDir?: () => string;
|
|
63
|
+
/** cwd of the invocation (a git repo sharing origin). Injected for tests. */
|
|
64
|
+
cwd?: () => string;
|
|
65
|
+
/** Enumerate borg-managed seats from the central cubes.json. */
|
|
66
|
+
listSeats?: () => Promise<Array<{
|
|
67
|
+
projectPath: string;
|
|
68
|
+
cube: ActiveCube;
|
|
69
|
+
}>>;
|
|
70
|
+
/** Probe ONE seat's eviction status using ITS OWN token (gh#877). */
|
|
71
|
+
probeSeat?: (sessionToken: string, apiUrl: string) => Promise<SeatStatus>;
|
|
72
|
+
/** realpath resolver — injected so tests can model symlink escape. */
|
|
73
|
+
realpath?: (p: string) => string;
|
|
74
|
+
stdout?: (line: string) => void;
|
|
75
|
+
stderr?: (line: string) => void;
|
|
76
|
+
}
|
|
77
|
+
export interface CleanupOptions {
|
|
78
|
+
/** Act on PRUNABLE rows. Default false = dry-run / report only. */
|
|
79
|
+
prune: boolean;
|
|
80
|
+
}
|
|
81
|
+
/** True iff a single gitignored path is a known-regenerable artifact. */
|
|
82
|
+
export declare function isRegenerableIgnored(ignoredPath: string): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Return the gitignored-local paths in `cwd` that are NOT regenerable — i.e.
|
|
85
|
+
* the precious-local-state files (`.env`, `.env.test`, `.dev.vars`, secrets,
|
|
86
|
+
* `data/`, …) that a prune would destroy forever. Non-empty → BLOCK (S4a).
|
|
87
|
+
*
|
|
88
|
+
* `git status --porcelain --ignored` emits ignored entries with a `!!` status
|
|
89
|
+
* prefix; git collapses an ignored directory to a single `!! dir/` entry, so
|
|
90
|
+
* segment-matching the reported path against the allowlist is sufficient.
|
|
91
|
+
*/
|
|
92
|
+
export declare function clobberClassIgnored(runSync: RunSync, cwd: string): string[];
|
|
93
|
+
/**
|
|
94
|
+
* True iff `child`'s canonicalized real path is STRICTLY under `parent`'s
|
|
95
|
+
* canonicalized real path — rejecting `..` traversal AND symlink escape (a
|
|
96
|
+
* symlinked worktree whose realpath is outside worktreesHome is NOT a prune
|
|
97
|
+
* candidate). Returns false (never under) if either path can't be resolved.
|
|
98
|
+
*/
|
|
99
|
+
export declare function isStrictlyUnder(realpath: (p: string) => string, parent: string, child: string): boolean;
|
|
100
|
+
/** Parse `git worktree list --porcelain` → absolute worktree paths (in order). */
|
|
101
|
+
export declare function parseWorktreeList(porcelain: string): string[];
|
|
102
|
+
/**
|
|
103
|
+
* Parse `git worktree list --porcelain` into `{path, branch}` entries (in
|
|
104
|
+
* order; primary first). `branch` is the ACTUAL checked-out branch name (from
|
|
105
|
+
* the `branch refs/heads/<name>` line) or null for a detached HEAD (no branch
|
|
106
|
+
* line). This is load-bearing (gh#882 CR blocker dcdd7fca): a drone cuts
|
|
107
|
+
* `fix/…`/`feat/…` off `wt-<suffix>` INSIDE the worktree, so the checked-out
|
|
108
|
+
* HEAD is commonly a feature branch with unpushed commits while `wt-<suffix>`
|
|
109
|
+
* stays at the merged base. The merged-gate + `git branch -d` MUST key on the
|
|
110
|
+
* actual HEAD branch, not the derived `wt-<suffix>`, or unmerged feature work
|
|
111
|
+
* is misclassified PRUNABLE.
|
|
112
|
+
*/
|
|
113
|
+
export declare function parseWorktreeEntries(porcelain: string): Array<{
|
|
114
|
+
path: string;
|
|
115
|
+
branch: string | null;
|
|
116
|
+
}>;
|
|
117
|
+
/**
|
|
118
|
+
* Build the cleanup report (read-only except the single up-front
|
|
119
|
+
* `git fetch origin`). Exported separately from the prune action so tests +
|
|
120
|
+
* the dry-run path share it.
|
|
121
|
+
*/
|
|
122
|
+
export declare function buildCleanupReport(deps: Required<CleanupDeps>): Promise<{
|
|
123
|
+
rows: CleanupRow[];
|
|
124
|
+
error?: string;
|
|
125
|
+
}>;
|
|
126
|
+
/**
|
|
127
|
+
* `borg cleanup [--prune]` entry point. Returns the process exit code.
|
|
128
|
+
* Dry-run (default): print the report, delete nothing. `--prune`: additionally
|
|
129
|
+
* remove PRUNABLE worktrees via `git worktree remove` (NO --force) +
|
|
130
|
+
* `git branch -d` (NO -D) — git's own refusals are the final backstop.
|
|
131
|
+
*/
|
|
132
|
+
export declare function runCleanup(deps?: CleanupDeps, opts?: CleanupOptions): Promise<number>;
|
|
133
|
+
export type ParseResult = {
|
|
134
|
+
ok: true;
|
|
135
|
+
options: CleanupOptions;
|
|
136
|
+
} | {
|
|
137
|
+
ok: false;
|
|
138
|
+
error: string;
|
|
139
|
+
};
|
|
140
|
+
/** Parse args after `borg cleanup`. Supports `--prune`; rejects anything else. */
|
|
141
|
+
export declare function parseCleanupArgs(rawArgs: string[]): ParseResult;
|
|
142
|
+
//# sourceMappingURL=cleanup-cmd.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import{spawnSync as P}from"node:child_process";import{realpathSync as D}from"node:fs";import{sep as R}from"node:path";import{homedir as V}from"node:os";import c from"chalk";import{classifyDirty as I,isMerged as A,worktreesHome as L}from"./worktree-lifecycle.js";import{readAllProjectIdentities as W}from"./cubes.js";import{whoami as x}from"./remote-client.js";import{DroneEvictedError as _,DroneFrozenError as C}from"./drone-lifecycle.js";const $="origin/main",F=new Set(["node_modules","dist","build",".next","coverage",".wrangler",".playwright-mcp"]),O=new Set([".DS_Store","worker-configuration.d.ts"]),j=[".log",".tsbuildinfo",".tmp"];function z(r){const n=r.replace(/\/+$/,""),e=n.split("/").filter(s=>s.length>0);if(e.some(s=>F.has(s)))return!0;const t=e[e.length-1]??n;return!!(O.has(t)||j.some(s=>t.endsWith(s)))}function G(r,n){const e=r("git",["status","--porcelain","--ignored"],n);if(e.status!==0)return["<git status --ignored failed \u2014 cannot verify clean>"];const t=[];for(const s of e.stdout.split(`
|
|
2
|
+
`)){if(!s.startsWith("!!"))continue;const i=s.slice(3).trim();i.length!==0&&(z(i)||t.push(i))}return t}const H={runSync:(r,n,e)=>{const t=P(r,n,{cwd:e,encoding:"utf-8"});return{status:t.status,stdout:t.stdout??"",stderr:t.stderr??""}},homeDir:()=>V(),cwd:()=>process.cwd(),listSeats:()=>W(),probeSeat:M,realpath:r=>D(r),stdout:r=>process.stdout.write(r),stderr:r=>process.stderr.write(r)};async function M(r,n){try{return await x(r,n),"live"}catch(e){return e instanceof _?"evicted":e instanceof C?"frozen":"indeterminate"}}function T(r,n,e){let t,s;try{t=r(n),s=r(e)}catch{return!1}if(s===t)return!1;const i=t.endsWith(R)?t:t+R;return s.startsWith(i)}function oe(r){return v(r).map(n=>n.path)}function v(r){const n=[];let e=null;for(const t of r.split(`
|
|
3
|
+
`))t.startsWith("worktree ")?(e&&n.push(e),e={path:t.slice(9).trim(),branch:null}):t.startsWith("branch ")&&e&&(e.branch=t.slice(7).trim().replace(/^refs\/heads\//,""));return e&&n.push(e),n}async function K(r,n,e,t){const{runSync:s}=r,i=I(s,n),l=i.staged.length+i.unstaged.length+i.untracked.length;if(l>0)return{reason:"SURVIVES-dirty",detail:`${l} uncommitted file(s)`};const u=G(s,n);if(u.length>0)return{reason:"SURVIVES-clobber",detail:`gitignored local state: ${u.slice(0,3).join(", ")}${u.length>3?` (+${u.length-3})`:""}`};if(e===null)return{reason:"SURVIVES-detached",detail:"detached HEAD \u2014 cannot verify merged"};if(!A(s,n,e,$))return{reason:"SURVIVES-unmerged",detail:`${e} not merged into ${$}`};switch(await r.probeSeat(t.sessionToken,t.apiUrl)){case"evicted":return{reason:"PRUNABLE",detail:"410 DRONE_EVICTED (clean + merged)"};case"frozen":return{reason:"SURVIVES-frozen",detail:"423 DRONE_FROZEN (reversible)"};case"live":return{reason:"SURVIVES-live",detail:"seat resolves (drone alive)"};default:return{reason:"UNKNOWN-indeterminate",detail:"probe returned 401/network/transient (or gh#877 not yet deployed) \u2014 not deleting"}}}async function X(r){const{runSync:n,realpath:e}=r,t=r.cwd();if(n("git",["rev-parse","--show-toplevel"],t).status!==0)return{rows:[],error:`not in a git repository (cwd: ${t})`};const s=n("git",["fetch","origin","--prune"],t);if(s.status!==0)return{rows:[],error:`git fetch origin failed: ${s.stderr.trim()}`};const i=n("git",["worktree","list","--porcelain"],t);if(i.status!==0)return{rows:[],error:`git worktree list failed: ${i.stderr.trim()}`};const l=v(i.stdout),u=new Map(l.map(a=>[a.path,a.branch])),f=l.map(a=>a.path),b=f[0],w=r.homeDir(),p=L(w),o=y(e,t),d=b?y(e,b):null,E=await r.listSeats(),S=new Map;for(const{projectPath:a,cube:h}of E){const g=y(e,a);g&&S.set(g,h)}const m=[];for(const a of f){const h=y(e,a);if(!h||d&&h===d)continue;const g=S.get(h),U=T(e,p,a);if(h===o){m.push({worktreePath:a,wtBranch:null,reason:"SURVIVES-self",detail:"current worktree"});continue}if(!U){g&&m.push({worktreePath:a,wtBranch:null,reason:"LEGACY-manual-review",detail:"borg seat outside worktreesHome (pre-gh#556 sibling)"});continue}if(!g){m.push({worktreePath:a,wtBranch:null,reason:"UNKNOWN-no-seat",detail:"no cubes.json seat \u2014 manual review"});continue}const k=u.get(a)??null,{reason:B,detail:N}=await K(r,a,k,g);m.push({worktreePath:a,wtBranch:k,reason:B,detail:N})}return{rows:m}}function y(r,n){try{return r(n)}catch{return null}}async function se(r={},n={prune:!1}){const e={...H,...r},{stdout:t,stderr:s,runSync:i}=e,{rows:l,error:u}=await X(e);if(u)return s(c.red(`\u25FC borg cleanup: ${u}
|
|
4
|
+
`)),1;const f=l.filter(o=>o.reason==="PRUNABLE"),b=l.filter(o=>o.reason!=="PRUNABLE");if(l.length===0)return t(c.blue(`\u25FC borg cleanup: no borg-managed worktrees found.
|
|
5
|
+
`)),0;t(c.bold(`\u25FC borg cleanup report:
|
|
6
|
+
`));for(const o of l){const d=o.reason==="PRUNABLE"?c.yellow(o.reason):c.gray(o.reason);t(` ${d} ${o.worktreePath}${o.detail?c.gray(` \u2014 ${o.detail}`):""}
|
|
7
|
+
`)}if(t(c.gray(`\u25FC ${f.length} prunable, ${b.length} kept.
|
|
8
|
+
`)),!n.prune)return f.length>0&&t(c.gray("\u25FC Dry-run \u2014 nothing deleted. Re-run with `--prune` to remove the PRUNABLE worktree(s).\n")),0;if(f.length===0)return t(c.blue(`\u25FC Nothing to prune.
|
|
9
|
+
`)),0;let w=0,p=0;for(const o of f){const d=i("git",["worktree","remove",o.worktreePath],e.cwd());if(d.status!==0){o.prune="remove-failed",p++,s(c.red(` \u2717 worktree remove ${o.worktreePath}: ${d.stderr.trim()}
|
|
10
|
+
`));continue}if(o.wtBranch){const E=i("git",["branch","-d",o.wtBranch],e.cwd());if(E.status!==0){o.prune="branch-delete-failed",s(c.yellow(` \u26A0 removed worktree but \`git branch -d ${o.wtBranch}\` refused: ${E.stderr.trim()}
|
|
11
|
+
`)),w++;continue}}o.prune="removed",w++,t(c.blue(` \u2713 pruned ${o.worktreePath}${o.wtBranch?` + branch ${o.wtBranch}`:""}
|
|
12
|
+
`))}return t(c.gray(`\u25FC Pruned ${w} worktree(s)${p>0?`, ${p} failed`:""}.
|
|
13
|
+
`)),p>0?1:0}function ie(r){let n=!1;for(const e of r)if(e==="--prune")n=!0;else return{ok:!1,error:`unexpected argument: ${e}. Usage: borg cleanup [--prune]`};return{ok:!0,options:{prune:n}}}export{X as buildCleanupReport,G as clobberClassIgnored,z as isRegenerableIgnored,T as isStrictlyUnder,ie as parseCleanupArgs,v as parseWorktreeEntries,oe as parseWorktreeList,se as runCleanup};
|
package/dist/cli-help.js
CHANGED
|
@@ -14,6 +14,7 @@ Usage:
|
|
|
14
14
|
borg assimilate --worktree <name> Spawn a worktree drone (in ~/.borg/worktrees/<repo>/<name>)
|
|
15
15
|
borg assimilate --model ollama:<model> Run the drone on an Ollama model (see \`borg assimilate --help\`)
|
|
16
16
|
borg sync [--prune] Sync this worktree's branch to origin/main
|
|
17
|
+
borg cleanup [--prune] Report (or --prune) worktrees orphaned by evicted drones
|
|
17
18
|
borg launch-all [cube] Launch all drone worktrees of a cube (default: active cube)
|
|
18
19
|
borg --cli claude|codex Choose agent CLI for this project
|
|
19
20
|
borg --version Show installed version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "borgmcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.32",
|
|
4
4
|
"description": "Coordinate AI coding agents in shared cubes. Works with Claude Code and Codex. Create projects, assign roles, and share a live activity log.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|