borgmcp 1.0.18 → 1.0.19
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/assimilate-cmd.d.ts
CHANGED
|
@@ -48,6 +48,8 @@ export interface AssimilateDeps {
|
|
|
48
48
|
pathExists: (p: string) => boolean;
|
|
49
49
|
cwd: () => string;
|
|
50
50
|
chdir: (p: string) => void;
|
|
51
|
+
homedir: () => string;
|
|
52
|
+
mkdirp: (dir: string) => void;
|
|
51
53
|
exec: (cmd: string, args: string[], cwd: string, env?: Record<string, string>) => Promise<number>;
|
|
52
54
|
stderr: (line: string) => void;
|
|
53
55
|
stdout: (line: string) => void;
|
package/dist/assimilate-cmd.js
CHANGED
|
@@ -1,40 +1,41 @@
|
|
|
1
|
-
import{dirname as J,basename as S
|
|
2
|
-
`),1}if(r.flags.worktree!==void 0){const t=
|
|
1
|
+
import{dirname as J,basename as S}from"node:path";import{randomUUID as V}from"node:crypto";import{roleSlug as Q,matchRoleByName as X,pickDefaultRole as Z}from"./role-resolver.js";import{deriveCubeName as ee,parseGitRemote as te,sanitizeRemoteUrl as re}from"./cube-name.js";import{validateName as H}from"./name-validator.js";import{renderAssimilationWelcome as ne}from"./assimilate-welcome.js";import{shellEscape as oe}from"./shell-escape.js";import{withCodexCwdArg as ie}from"./codex-remote.js";import{buildAgentKickoffPrompt as ae,recordCodexWakeTarget as se,socketPathFromRemoteArgs as le}from"./codex-launch.js";import{perWorktreeBranchName as O,adoptWorktree as ce,computeWorktreePath as F}from"./worktree-lifecycle.js";import{codexBorgSessionConfigArgs as ue}from"./launch-gate.js";async function Te(r,e){if(r.role!==void 0){const t=H(r.role);if(!t.ok)return e.stderr(t.error+`
|
|
2
|
+
`),1}if(r.flags.worktree!==void 0){const t=H(r.flags.worktree);if(!t.ok)return e.stderr(t.error+`
|
|
3
3
|
`),1}let i=await e.getCachedAuth();if(!i){if(!e.isTTY()&&!r.flags.yes)return e.stderr("borg setup required and stdin is non-interactive. Run `borg setup` first in an interactive terminal, then `borg assimilate`.\n"),1;i=await e.runSetup()}const a=e.findProjectRoot(e.cwd());let n;if(r.flags.cubeName)n=r.flags.cubeName;else{const t=e.runSync("git",["remote","get-url","origin"],a),o=t.status===0?t.stdout:null;if(n=ee(a,o),o){const c=re(o),m=c?te(c):null;c&&!m&&n&&e.stderr(`couldn't parse git remote '${c}' \u2014 using directory name '${n}' as cube name
|
|
4
|
-
`)}}let s=null;if(n&&n.includes("@")&&n.includes(":")){const t=n.lastIndexOf(":");s={ownerEmail:n.substring(0,t),cubeName:n.substring(t+1)},n=s.cubeName}const
|
|
4
|
+
`)}}let s=null;if(n&&n.includes("@")&&n.includes(":")){const t=n.lastIndexOf(":");s={ownerEmail:n.substring(0,t),cubeName:n.substring(t+1)},n=s.cubeName}const p=e.cwd();e.stderr(`Checking your cubes\u2026
|
|
5
5
|
`);let R;try{R=await e.listCubes(i.apiUrl,i.token)}catch(t){const o=t instanceof Error?t.message:String(t);if(o.includes("Authentication required")||o.includes("Authentication expired"))e.stderr(`Re-authenticating...
|
|
6
6
|
`),i=await e.runSetup(),R=await e.listCubes(i.apiUrl,i.token);else throw t}const E=R.find(t=>t.name===n);if(!E&&s)return e.stderr(`No cube named '${s.cubeName}' accessible to you owned by '${s.ownerEmail}'. Did you accept their invite? See borgmcp.ai/dashboard.
|
|
7
|
-
`),1;let l,A;if(E)l=await e.getCube(i.apiUrl,i.token,E.id),A=!1;else{let t;if(r.flags.template)t=r.flags.template;else if(r.flags.noTemplate)t=void 0;else if(e.isTTY())if(r.flags.yes)t="starter";else{const o=await e.listTemplates(i.apiUrl,i.token),c=["First drone joining a new cube. Apply a template?"];o.forEach((y
|
|
7
|
+
`),1;let l,A;if(E)l=await e.getCube(i.apiUrl,i.token,E.id),A=!1;else{let t;if(r.flags.template)t=r.flags.template;else if(r.flags.noTemplate)t=void 0;else if(e.isTTY())if(r.flags.yes)t="starter";else{const o=await e.listTemplates(i.apiUrl,i.token),c=["First drone joining a new cube. Apply a template?"];o.forEach((k,y)=>{const C=y===0?" (default)":"";c.push(` ${y+1}) ${k.name}${C} \u2014 ${k.description}`)}),c.push(` ${o.length+1}) skip \u2014 no template`);const m=(await e.prompt(c.join(`
|
|
8
8
|
`)+`
|
|
9
|
-
[1]: `)).trim(),
|
|
10
|
-
`),1;t=
|
|
9
|
+
[1]: `)).trim(),b=m===""?1:parseInt(m,10);if(Number.isNaN(b)||b<1||b>o.length+1)return e.stderr(`invalid choice "${m}"
|
|
10
|
+
`),1;t=b<=o.length?o[b-1].name:void 0}else{if(!r.flags.yes)return e.stderr(`cube creation needs a template choice but stdin is non-interactive.
|
|
11
11
|
Pass --template <name>, --no-template, or --yes (defaults to starter).
|
|
12
12
|
`),1;t="starter"}e.stderr(n?`Creating cube '${n}'\u2026
|
|
13
13
|
`:`Creating your cube\u2026
|
|
14
14
|
`),l=await e.createCube(i.apiUrl,i.token,t?{name:n??void 0,template:t}:{name:n??void 0}),A=!0}let d;if(r.role!==void 0){if(d=X(l.roles,r.role),!d){const t=l.roles.map(m=>m.name).join(", "),o=ge(r.role,l.roles.map(m=>m.name)),c=o?` Did you mean "${o}"?`:"";return e.stderr(`no role matching "${r.role}" in cube "${l.name}". Available: ${t}.${c}
|
|
15
15
|
(Use --template <name> on first-drone setup or run \`borg:create-role\` from inside Claude.)
|
|
16
16
|
`),1}}else if(d=Z(l.roles,{isFirstDrone:A}),!d)return e.stderr(`cube "${l.name}" has no default or human-seat role; cannot infer a role. Either pass a role argument explicitly (e.g. \`borg assimilate builder\`) or run \`borg:create-role\` from inside Claude to set up roles.
|
|
17
|
-
`),1;const
|
|
17
|
+
`),1;const v=await e.getActiveCube();let N;if(v&&r.flags.here)if(v.cubeId===l.id)N=v.droneId;else return e.stderr(`this directory already hosts an active drone; remove --here or run from a fresh worktree
|
|
18
18
|
`),1;e.stderr(`Joining cube '${l.name}' as ${d.name}\u2026
|
|
19
19
|
`);let u;try{u=await e.assimilate(i.apiUrl,i.token,{cube_id:l.id,role_id:d.id,hostname:e.getHostname(),...N?{prior_drone_id:N}:{}})}catch(t){const o=t instanceof Error?t.message:String(t);return e.stderr(`assimilate failed: ${o}
|
|
20
|
-
`),1}const
|
|
21
|
-
`)
|
|
22
|
-
`);const M=r.flags.worktree!==void 0||
|
|
20
|
+
`),1}const $=l.roles.find(t=>t.id===u.role_id)??d;u.reattached?e.stderr(`re-attached to existing seat ${u.drone_label} (session token rotated, no new drone minted)
|
|
21
|
+
`):$.id!==d.id&&e.stderr(`Note: your invite didn't grant the "${d.name}" role \u2014 assimilated as "${$.name}" instead.
|
|
22
|
+
`);const M=r.flags.worktree!==void 0||v!==null&&!r.flags.here;let f=null;if(M){const t=e.runSync("git",["rev-parse","--verify","HEAD"],a);if(t.status!==0)return e.stderr(`sibling worktree spawn requires HEAD pointing at a commit.
|
|
23
23
|
Fix: create at least one commit (\`git commit --allow-empty -m "initial"\`)
|
|
24
24
|
OR: pass --here to skip the sibling spawn and use the current directory
|
|
25
|
-
`),1;e.runSync("git",["fetch","origin"],a);let o="origin/main";e.runSync("git",["rev-parse","--verify","origin/main"],a).status!==0&&e.runSync("git",["rev-parse","--verify","origin/master"],a).status===0&&(o="origin/master");const m=t.stdout.trim(),
|
|
26
|
-
`);const
|
|
27
|
-
`),1;e.
|
|
28
|
-
`),e.
|
|
25
|
+
`),1;e.runSync("git",["fetch","origin"],a);let o="origin/main";e.runSync("git",["rev-parse","--verify","origin/main"],a).status!==0&&e.runSync("git",["rev-parse","--verify","origin/master"],a).status===0&&(o="origin/master");const m=t.stdout.trim(),b=e.runSync("git",["rev-parse",o],a).stdout.trim();m!==b&&e.stderr(`note: local HEAD (${m.slice(0,7)}) differs from ${o} (${b.slice(0,7)}); new worktree will start on ${o}
|
|
26
|
+
`);const k=S(a),y=r.flags.worktree??Q($.name);if(y.length===0)return e.stderr(`cannot derive a worktree name from role "${$.name}"; pass an explicit --worktree <name>
|
|
27
|
+
`),1;const C=e.homedir();let h=F(C,k,y),W=2;for(;e.pathExists(h)||de(e,a,h);)h=F(C,k,y,W),W++;e.mkdirp(J(h));const D=O(S(h),k),L=e.runSync("git",["worktree","add","-b",D,h,o],a);if(L.status!==0)return e.stderr(`git worktree add failed: ${B(L.stderr)}
|
|
28
|
+
`),1;e.stderr(`spawned sibling worktree at ${h} on branch ${D} (${o}); original dir is registered as active (edit ~/.config/borgmcp/cubes.json if stale).
|
|
29
|
+
`),e.chdir(h),e.stderr(me(h,D,a)),f=e.cwd()}try{await e.setActiveCube({cubeId:u.cube_id,droneId:u.drone_id,name:l.name,sessionToken:u.session_token,droneLabel:u.drone_label,apiUrl:i.apiUrl})}catch(t){const o=t instanceof Error?t.message:String(t);if(e.stderr(`setActiveCube failed: ${o}
|
|
29
30
|
`),f){const c=e.runSync("git",["worktree","remove","--force",f],a);c.status===0?e.stderr(`rolled back spawned worktree at ${f}
|
|
30
31
|
`):e.stderr(`manual cleanup needed: \`git worktree remove --force ${f}\` (rollback attempt failed: ${B(c.stderr).trim()||"unknown"})
|
|
31
|
-
`)}return 1}e.setTerminalTitle(u.drone_label,l.name);const Y=e.isTTY()&&!process.env.NO_COLOR&&!process.env.CI;e.stdout(ne(
|
|
32
|
-
`)}if(!f){e.runSync("git",["fetch","origin","--prune"],g);const t=
|
|
32
|
+
`)}return 1}e.setTerminalTitle(u.drone_label,l.name);const Y=e.isTTY()&&!process.env.NO_COLOR&&!process.env.CI;e.stdout(ne($.name,l.name,Y));const w=await e.resolveCli(r.flags.cli),g=e.cwd();try{e.installProjectSessionHook(g)}catch{e.stderr(`warning: could not install the project-local SessionStart hook in ${g}; it will be re-attempted on the next borg launch
|
|
33
|
+
`)}if(!f){e.runSync("git",["fetch","origin","--prune"],g);const t=O(S(g),S(a)),o=ce(e.runSync,g,t,"origin/main");o.action==="adopted"?(e.stderr(`worktree: adopted branch ${t} at origin/main
|
|
33
34
|
`),e.stderr(fe(g,t))):o.message&&e.stderr(`worktree sync: ${o.message}
|
|
34
|
-
`)}await e.probeMcpReady()||e.stderr(`warning: borg-mcp readiness probe did not complete within the timeout; launching ${
|
|
35
|
-
`);const G=e.getInboxPath(u.cube_id,u.drone_id),T=
|
|
36
|
-
`),
|
|
37
|
-
Agent exited. You were working in ${f}; your shell is back in ${
|
|
35
|
+
`)}await e.probeMcpReady()||e.stderr(`warning: borg-mcp readiness probe did not complete within the timeout; launching ${w} anyway \u2014 the kickoff prompt's ToolSearch fallback will recover if the MCP server takes longer to start.
|
|
36
|
+
`);const G=e.getInboxPath(u.cube_id,u.drone_id),T=w==="codex"?`borg-wake-${V()}`:null,q=w==="claude"?`If you haven't yet, arm a persistent Monitor running the command \`borg-inbox-monitor ${G}\` 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. `:"";let P,U=[],x,j,I=null,_=null;if(w==="codex"){const t=await e.prepareCodexRemoteLaunch();t.warning?(e.stderr(`warning: ${t.warning}
|
|
37
|
+
`),P="\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."):P="Codex wake-path capability check passed: remote-control socket established for this session.",U=t.args,j=Object.keys(t.env).length>0?t.env:void 0,I=le(t.args),_=t.server?.cleanup??null}x=[ae({cli:w,codexWakeNonce:T,monitorClause:q,codexWakePathClause:P})],w==="codex"&&(x=[...ue(),...U,...ie(x,g)]);const z=e.exec(w,x,g,{...j??{},BORG_SESSION:"1"});w==="codex"&&I&&T&&se({deps:e,cubeId:u.cube_id,droneId:u.drone_id,socketPath:I,cwd:g,previewNeedle:T,launchedAtSeconds:Math.floor(Date.now()/1e3)});const K=await z;if(_)try{_()}catch{}return f&&p!==f&&e.stderr(`
|
|
38
|
+
Agent exited. You were working in ${f}; your shell is back in ${p}.
|
|
38
39
|
To return:
|
|
39
40
|
cd ${oe(f)}
|
|
40
41
|
`),K}function me(r,e,i){return`
|
|
@@ -42,4 +43,4 @@ WORKTREE STEERING: You are in worktree ${r} on branch ${e}. Do ALL work HERE \u2
|
|
|
42
43
|
`}function fe(r,e){return`
|
|
43
44
|
WORKTREE STEERING: This checkout is now on branch ${e}. Do ALL work HERE in ${r} \u2014 cut your feature branch (fix/.../feat/...) off ${e}, use relative paths / your cwd.
|
|
44
45
|
`}function B(r){return r.replace(/[\x00-\x1F\x7F]/g,"")}function de(r,e,i){const a=r.runSync("git",["worktree","list","--porcelain"],e);return a.status!==0?!1:a.stdout.split(`
|
|
45
|
-
`).some(n=>n===`worktree ${i}`)}function ge(r,e){if(e.length===0)return null;const i=r.toLowerCase();let a=null;for(const n of e){const s=he(i,n.toLowerCase());s<=2&&(a===null||s<a.distance)&&(a={name:n,distance:s})}return a?a.name:null}function he(r,e){if(r===e)return 0;if(r.length===0)return e.length;if(e.length===0)return r.length;const i=new Array(e.length+1),a=new Array(e.length+1);for(let n=0;n<=e.length;n++)i[n]=n;for(let n=1;n<=r.length;n++){a[0]=n;for(let s=1;s<=e.length;s++){const
|
|
46
|
+
`).some(n=>n===`worktree ${i}`)}function ge(r,e){if(e.length===0)return null;const i=r.toLowerCase();let a=null;for(const n of e){const s=he(i,n.toLowerCase());s<=2&&(a===null||s<a.distance)&&(a={name:n,distance:s})}return a?a.name:null}function he(r,e){if(r===e)return 0;if(r.length===0)return e.length;if(e.length===0)return r.length;const i=new Array(e.length+1),a=new Array(e.length+1);for(let n=0;n<=e.length;n++)i[n]=n;for(let n=1;n<=r.length;n++){a[0]=n;for(let s=1;s<=e.length;s++){const p=r[n-1]===e[s-1]?0:1;a[s]=Math.min(a[s-1]+1,i[s]+1,i[s-1]+p)}for(let s=0;s<=e.length;s++)i[s]=a[s]}return i[e.length]}export{Te as runAssimilate,B as safeStderr,ge as suggestRoleName};
|
package/dist/assimilate-deps.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{spawnSync as m,spawn as l}from"node:child_process";import{existsSync as f}from"node:fs";import{hostname as
|
|
1
|
+
import{spawnSync as m,spawn as l}from"node:child_process";import{existsSync as f,mkdirSync as b}from"node:fs";import{hostname as h,homedir as C}from"node:os";import{createInterface as d}from"node:readline/promises";import{API_URL as u,getValidToken as p,listCubes as _,getCube as T,createCube as y,assimilate as w,listTemplates as k}from"./remote-client.js";import{findProjectRoot as g,getActiveCube as x,setActiveCube as v,inboxPathForDrone as S,setCodexWakeTarget as A}from"./cubes.js";import{authenticateWithGoogle as P}from"./auth.js";import{addProjectSessionStartHook as R}from"./config-utils.js";import{setTerminalTitle as U}from"./terminal-title.js";import{defaultCliChoiceDeps as j,resolveCliChoice as L}from"./cli-platform.js";import{prepareCodexRemoteLaunch as D,defaultCodexRemoteDeps as H}from"./codex-remote.js";import{findLoadedCodexThread as I}from"./codex-app-server.js";function K(){return{runSync:(e,r,o)=>{const t=m(e,r,{cwd:o,encoding:"utf-8"});return{status:t.status,stdout:t.stdout??"",stderr:t.stderr??""}},pathExists:e=>f(e),cwd:()=>process.cwd(),chdir:e=>process.chdir(e),homedir:()=>C(),mkdirp:e=>b(e,{recursive:!0}),exec:(e,r,o,t)=>new Promise((s,i)=>{const a=l(e,r,{cwd:o,stdio:"inherit",shell:!1,env:t?{...process.env,...t}:process.env});a.on("error",i),a.on("exit",n=>s(n??0))}),stderr:e=>process.stderr.write(e),stdout:e=>process.stdout.write(e),prompt:async e=>{const r=d({input:process.stdin,output:process.stdout});try{return await r.question(e)}finally{r.close()}},isTTY:()=>process.stdin.isTTY===!0,getHostname:()=>h(),setTerminalTitle:(e,r)=>{U({label:e,cubeName:r},r)},getActiveCube:()=>x(),setActiveCube:e=>v(e),findProjectRoot:e=>g(e),installProjectSessionHook:e=>{R(e)},getCachedAuth:async()=>{try{return{token:await p(),apiUrl:u}}catch{return null}},runSetup:async()=>(await P(),{token:await p(),apiUrl:u}),listCubes:async(e,r)=>{const{cubes:o}=await _();return o.map(t=>({id:t.id,name:t.name}))},getCube:async(e,r,o)=>{const t=await T(o);return{id:t.id,name:t.name,roles:t.roles}},createCube:async(e,r,o)=>{const t=await y(o.name,"",o.template?{template:o.template}:void 0);return{id:t.id,name:t.name,roles:t.roles}},assimilate:async(e,r,o)=>{const t=await w({cube_id:o.cube_id,role_id:o.role_id,...o.prior_drone_id?{prior_drone_id:o.prior_drone_id}:{}},void 0,o.hostname??null);return{cube_id:t.cube.id,drone_id:t.drone.id,drone_label:t.drone.label,session_token:t.sessionToken,role_id:t.role.id,reattached:t.reattached===!0}},listTemplates:async(e,r)=>{const{templates:o}=await k();return o.map(t=>({name:t.name,description:t.description}))},getInboxPath:(e,r)=>S(e,r),probeMcpReady:()=>new Promise(e=>{const r=l("borg-mcp",[],{stdio:["pipe","pipe","pipe"],shell:!1});let o="",t=!1;const s=n=>{if(!t){t=!0;try{r.kill("SIGTERM")}catch{}e(n)}},i=setTimeout(()=>s(!1),2e3);r.on("error",()=>{clearTimeout(i),s(!1)}),r.on("exit",()=>{clearTimeout(i),s(t)}),r.stdout?.on("data",n=>{o+=n.toString("utf-8");for(const c of o.split(`
|
|
2
2
|
`))if(c.includes('"protocolVersion"')&&c.includes('"result"')){clearTimeout(i),s(!0);return}});const a=JSON.stringify({jsonrpc:"2.0",id:1,method:"initialize",params:{protocolVersion:"2024-11-05",capabilities:{},clientInfo:{name:"borg-assimilate-probe",version:"0.9.3"}}});try{r.stdin?.write(a+`
|
|
3
|
-
`)}catch{clearTimeout(i),s(!1)}}),resolveCli:e=>
|
|
3
|
+
`)}catch{clearTimeout(i),s(!1)}}),resolveCli:e=>L(e,j(async r=>{const o=d({input:process.stdin,output:process.stdout});try{return await o.question(r)}finally{o.close()}},()=>process.stdin.isTTY===!0)),prepareCodexRemoteLaunch:()=>D(H()),setCodexWakeTarget:A,findLoadedCodexThread:I}}export{K as buildDefaultAssimilateDeps};
|
package/dist/cli-help.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function r(e){return e==="--help"||e==="-h"}function
|
|
1
|
+
function r(e){return e==="--help"||e==="-h"}function o(e){return`borgmcp ${e} \u2014 run several AI coding agents on one project, together.
|
|
2
2
|
They coordinate through a shared log (a "cube"). For Claude Code & Codex.
|
|
3
3
|
|
|
4
4
|
Docs & quickstart: https://borgmcp.ai/get-started
|
|
@@ -11,13 +11,13 @@ Usage:
|
|
|
11
11
|
borg setup Set up OAuth + register MCP server
|
|
12
12
|
borg setup --no-browser Set up from SSH/headless terminals
|
|
13
13
|
borg assimilate [role] Join a cube (creates one if needed)
|
|
14
|
-
borg assimilate --worktree <name> Spawn a
|
|
14
|
+
borg assimilate --worktree <name> Spawn a worktree drone (in ~/.borg/worktrees/<repo>/<name>)
|
|
15
15
|
borg sync [--prune] Sync this worktree's branch to origin/main
|
|
16
16
|
borg --cli claude|codex Choose agent CLI for this project
|
|
17
17
|
borg --version Show installed version
|
|
18
18
|
|
|
19
19
|
All other arguments are passed through to the selected agent CLI.
|
|
20
|
-
`}function
|
|
20
|
+
`}function n(e){return`borg setup (borgmcp ${e}) \u2014 set up OAuth + register the borg MCP server
|
|
21
21
|
|
|
22
22
|
Borg MCP needs Claude Code or Codex installed first.
|
|
23
23
|
|
|
@@ -28,4 +28,4 @@ Usage:
|
|
|
28
28
|
for SSH / headless / container terminals. Alias: --device.
|
|
29
29
|
Auto-detected on SSH/headless; this forces it.
|
|
30
30
|
borg setup --help Show this help
|
|
31
|
-
`}export{r as isHelpFlag,
|
|
31
|
+
`}export{r as isHelpFlag,n as setupHelpText,o as topLevelHelpText};
|
|
@@ -28,6 +28,29 @@ export type RunSync = (cmd: string, args: string[], cwd?: string) => {
|
|
|
28
28
|
* `wt-myrepo-feature`).
|
|
29
29
|
*/
|
|
30
30
|
export declare function perWorktreeBranchName(worktreeBasename: string, repoBasename: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* gh#556 Part 1 — the home for NEW drone worktrees: `<homeDir>/.borg/worktrees`.
|
|
33
|
+
* (`~/.borg` is the established borg home — it already holds the encrypted
|
|
34
|
+
* credentials file, see config.ts.)
|
|
35
|
+
*/
|
|
36
|
+
export declare function worktreesHome(homeDir: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* gh#556 Part 1 — where a NEW drone worktree lives:
|
|
39
|
+
* `<homeDir>/.borg/worktrees/<repoBase>/<suffix>` (collision variant `<suffix>-<n>`
|
|
40
|
+
* for n>=2; the caller loops n until the path is free).
|
|
41
|
+
*
|
|
42
|
+
* Pure (homeDir injected) so the path scheme + collision dedup + containment are
|
|
43
|
+
* unit-testable without touching $HOME or spawning git.
|
|
44
|
+
*
|
|
45
|
+
* Path-safety / no-traversal: `suffix` is validated upstream BEFORE it reaches here —
|
|
46
|
+
* `--worktree` via validateName (NAME_RE excludes `.`/`/`) or the role default via
|
|
47
|
+
* roleSlug (strips everything but `[a-z0-9-]`); `repoBase` is a single `basename(...)`
|
|
48
|
+
* component. So the result is always CONTAINED under `worktreesHome(homeDir)`.
|
|
49
|
+
* As defense-in-depth this throws on an EMPTY suffix — an empty leaf would let
|
|
50
|
+
* `join` collapse the path up to the repo-level dir (the degenerate-path bug); the
|
|
51
|
+
* caller also guards empty before calling, fail-loud.
|
|
52
|
+
*/
|
|
53
|
+
export declare function computeWorktreePath(homeDir: string, repoBase: string, suffix: string, n?: number): string;
|
|
31
54
|
/** True iff the working tree is clean (`git status --porcelain` empty). */
|
|
32
55
|
export declare function isCleanTree(runSync: RunSync, cwd: string): boolean;
|
|
33
56
|
export interface DirtyClassification {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function
|
|
2
|
-
`)){if(!n.trim())continue;const
|
|
1
|
+
import{join as c}from"node:path";function h(e,o){const t=`${o}-`;return`wt-${e.startsWith(t)?e.slice(t.length):e}`}function p(e){return c(e,".borg","worktrees")}function x(e,o,t,r){if(t.length===0)throw new Error("computeWorktreePath: suffix must be non-empty (empty leaf would collapse the path to the repo-level dir)");const n=r!==void 0&&r>=2?`${t}-${r}`:t;return c(p(e),o,n)}function d(e,o){const t=e("git",["status","--porcelain"],o);return t.status===0&&t.stdout.trim()===""}const g=/^\.claude\//;function $(e,o){const t=e("git",["status","--porcelain"],o),r={staged:[],unstaged:[],untracked:[],localConfig:[]};if(t.status!==0)return r;for(const n of t.stdout.split(`
|
|
2
|
+
`)){if(!n.trim())continue;const s=n.slice(3);if(n.startsWith("??"))r.untracked.push(s);else{const a=n[0],u=n[1];a!==" "&&a!=="?"&&r.staged.push(s),u!==" "&&u!=="?"&&r.unstaged.push(s)}g.test(s)&&r.localConfig.push(s)}return r}function f(e,o,t,r){return e("git",["merge-base","--is-ancestor",t,r],o).status===0}function i(e,o,t,r){return e("git",["merge-base","--is-ancestor",t,r],o).status===0}function k(e,o,t,r){if(!d(e,o))return{action:"skipped-dirty",message:"uncommitted changes present; sync skipped (nothing discarded)"};if(!f(e,o,t,r))return{action:"skipped-diverged",message:`${t} has diverged from ${r}; resolve manually (no auto-merge/rebase)`};const n=e("git",["rev-list","--count",`${t}..${r}`],o);return n.status===0&&n.stdout.trim()==="0"?{action:"already-current"}:e("git",["merge","--ff-only",r],o).status!==0?{action:"skipped-diverged",message:"ff-only merge unexpectedly failed"}:{action:"fast-forwarded"}}function l(e,o,t){return e("git",["rev-parse","--verify","--quiet",`refs/heads/${t}`],o).status===0}function b(e,o,t,r){return d(e,o)?i(e,o,"HEAD",r)?l(e,o,t)&&!i(e,o,t,r)?{action:"blocked-target-unmerged",message:`branch ${t} exists with commits not on ${r}; resolve before adopting (a force-switch would discard them)`}:(e("git",["switch","-C",t,r],o),{action:"adopted"}):{action:"blocked-unmerged",message:`current HEAD has commits not on ${r}; commit/push or set aside before adopting`}:{action:"skipped-dirty",message:"uncommitted changes present; not switching (nothing discarded)"}}function v(e,o,t,r,n={prune:!1}){return i(e,o,t,r)?n.prune?(e("git",["branch","-d",t],o),{action:"pruned",branch:t}):{action:"announced",branch:t,message:`${t} is merged into ${r} and can be pruned: \`git branch -d ${t}\` (or re-run with --prune)`}:{action:"not-merged",branch:t}}export{b as adoptWorktree,$ as classifyDirty,v as cleanupMerged,x as computeWorktreePath,d as isCleanTree,f as isFastForward,i as isMerged,l as localBranchExists,h as perWorktreeBranchName,k as syncWorktree,p as worktreesHome};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "borgmcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.19",
|
|
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",
|