borgmcp 1.0.18 → 1.0.20
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 +2 -0
- package/dist/assimilate-cmd.js +22 -21
- package/dist/assimilate-deps.js +2 -2
- package/dist/backends/launch-all-pastelist.d.ts +4 -0
- package/dist/backends/launch-all-pastelist.js +4 -0
- package/dist/backends/launch-all-tmux.d.ts +12 -0
- package/dist/backends/launch-all-tmux.js +1 -0
- package/dist/backends/launch-all-windows.d.ts +9 -0
- package/dist/backends/launch-all-windows.js +12 -0
- package/dist/claude.js +11 -10
- package/dist/cli-help.js +5 -4
- package/dist/cubes.d.ts +16 -0
- package/dist/cubes.js +4 -4
- package/dist/launch-all-cmd.d.ts +11 -0
- package/dist/launch-all-cmd.js +29 -0
- package/dist/launch-all-command.d.ts +16 -0
- package/dist/launch-all-command.js +1 -0
- package/dist/launch-all-deps.d.ts +83 -0
- package/dist/launch-all-deps.js +1 -0
- package/dist/launch-all-discovery.d.ts +33 -0
- package/dist/launch-all-discovery.js +4 -0
- package/dist/launch-all-locks.d.ts +21 -0
- package/dist/launch-all-locks.js +1 -0
- package/dist/parse-launch-all-args.d.ts +22 -0
- package/dist/parse-launch-all-args.js +1 -0
- package/dist/worktree-lifecycle.d.ts +23 -0
- package/dist/worktree-lifecycle.js +2 -2
- package/package.json +1 -1
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};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DroneCandidate } from '../launch-all-discovery.js';
|
|
2
|
+
import type { LaunchAllDeps } from '../launch-all-deps.js';
|
|
3
|
+
export declare function runPastelistBackend(candidates: DroneCandidate[], borgPath: string, deps: LaunchAllDeps): void;
|
|
4
|
+
//# sourceMappingURL=launch-all-pastelist.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DroneCandidate } from '../launch-all-discovery.js';
|
|
2
|
+
import type { LaunchAllDeps } from '../launch-all-deps.js';
|
|
3
|
+
export interface TmuxOpts {
|
|
4
|
+
sessionName: string;
|
|
5
|
+
borgPath: string;
|
|
6
|
+
/** 'attach' = attach-session; 'switch' = switch-client (nested tmux); 'none' = skip. */
|
|
7
|
+
attachMode: 'attach' | 'switch' | 'none';
|
|
8
|
+
/** ISO-8601 captured before the first send-keys (lock-marker launchedAt). */
|
|
9
|
+
launchedAtISO: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function runTmuxBackend(candidates: DroneCandidate[], opts: TmuxOpts, deps: LaunchAllDeps): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=launch-all-tmux.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{buildLaunchCommand as h}from"../launch-all-command.js";import{writeLockMarker as d}from"../launch-all-locks.js";async function k(o,a,t){const{sessionName:e,borgPath:s,attachMode:c,launchedAtISO:u}=a,m=t.runSyncExitCode("tmux",["has-session","-t",e])===0;for(let r=0;r<o.length;r++){const n=o[r];let i;r===0&&!m?i=t.runSync("tmux",["new-session","-d","-P","-F","#{window_id}","-s",e,"-c",n.worktreeDir]).trim():i=t.runSync("tmux",["new-window","-P","-F","#{window_id}","-t",e,"-c",n.worktreeDir]).trim(),t.runSync("tmux",["rename-window","-t",i,n.droneLabel]);const w=h(n.worktreeDir,s,{keepOpenOnFail:!0});t.runSync("tmux",["send-keys","-t",i,w,"Enter"]),d(t,n.cubeId,n.droneLabel,n.worktreeDir,u)}c==="switch"?t.attachInteractive("tmux",["switch-client","-t",e]):c==="attach"&&t.attachInteractive("tmux",["attach-session","-t",e])}export{k as runTmuxBackend};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DroneCandidate } from '../launch-all-discovery.js';
|
|
2
|
+
import type { LaunchAllDeps } from '../launch-all-deps.js';
|
|
3
|
+
export interface WindowsOpts {
|
|
4
|
+
borgPath: string;
|
|
5
|
+
platform: NodeJS.Platform;
|
|
6
|
+
launchedAtISO: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function runWindowsBackend(candidates: DroneCandidate[], opts: WindowsOpts, deps: LaunchAllDeps): Promise<void>;
|
|
9
|
+
//# sourceMappingURL=launch-all-windows.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import{buildLaunchCommand as l}from"../launch-all-command.js";import{writeLockMarker as m}from"../launch-all-locks.js";function s(o){return o.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}class u extends Error{}function f(o,t,e){const a=e.pathExists("/Applications/iTerm.app"),i=e.pathExists("/Applications/Terminal.app");if(!a&&!i)throw new u(`borg launch-all: --mode windows requires a compatible terminal app.
|
|
2
|
+
Not found: iTerm.app, Terminal.app
|
|
3
|
+
Install iTerm2 (https://iterm2.com) or use --mode tmux (brew install tmux).
|
|
4
|
+
`);for(const n of o){const r=l(n.worktreeDir,t.borgPath),c=a?`tell application "iTerm"
|
|
5
|
+
tell current window to create tab with default profile command "${s(r)}"
|
|
6
|
+
end tell`:`tell application "Terminal"
|
|
7
|
+
do script "${s(r)}"
|
|
8
|
+
activate
|
|
9
|
+
end tell`;e.runSync("osascript",["-e",c]),m(e,n.cubeId,n.droneLabel,n.worktreeDir,t.launchedAtISO)}}function p(o,t,e){const a=e.getEnv("BORG_TERMINAL")||e.getEnv("TERMINAL"),i=["gnome-terminal","konsole","kitty","wezterm","xterm"];let n=a;if(!n){for(const r of i)if(e.runSyncExitCode("which",[r])===0){n=r;break}}if(!n)throw new u(`borg launch-all: --mode windows requires a terminal emulator.
|
|
10
|
+
Not found. Set $BORG_TERMINAL=<path> or use --mode tmux.
|
|
11
|
+
`);for(const r of o){const c=l(r.worktreeDir,t.borgPath,{keepOpenOnFail:!0});e.runSync(n,["-e","sh","-c",c]),m(e,r.cubeId,r.droneLabel,r.worktreeDir,t.launchedAtISO)}}const d=/[\x00-\x1f\x7f]/;async function b(o,t,e){const a=o.filter(i=>d.test(i.worktreeDir)?(e.stderr(`skipping ${i.droneLabel} (${JSON.stringify(i.worktreeDir)}): worktree path contains a control character \u2014 unsafe for --mode windows; use --mode tmux instead.
|
|
12
|
+
`),!1):!0);t.platform==="darwin"?f(a,t,e):p(a,t,e)}export{b as runWindowsBackend};
|
package/dist/claude.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{spawn as S}from"child_process";import{randomUUID as y}from"node:crypto";import{basename as A}from"node:path";import{createInterface as P}from"node:readline/promises";import
|
|
3
|
-
`)})();if((process.argv[2]==="--help"||process.argv[2]==="-h")&&(process.stdout.write(
|
|
4
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const
|
|
5
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const
|
|
6
|
-
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const
|
|
7
|
-
\u25FC
|
|
8
|
-
`)}`)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
import{spawn as S}from"child_process";import{randomUUID as y}from"node:crypto";import{basename as A}from"node:path";import{createInterface as P}from"node:readline/promises";import o from"chalk";import{findProjectRoot as T,getActiveCube as R,inboxPathForDrone as I,setCodexWakeTarget as D}from"./cubes.js";import{handleVersionFlag as E,getPackageVersion as g}from"./version.js";import{isHelpFlag as O,setupHelpText as F,topLevelHelpText as L}from"./cli-help.js";import{runSpawn as N}from"./spawn.js";import{parseSyncArgs as H,runSync as M}from"./sync.js";import{parseAssimilateArgs as B}from"./parse-assimilate-args.js";import{runAssimilate as G}from"./assimilate-cmd.js";import{buildDefaultAssimilateDeps as _}from"./assimilate-deps.js";import{parseLaunchAllArgs as W}from"./parse-launch-all-args.js";import{runLaunchAll as U}from"./launch-all-cmd.js";import{buildDefaultLaunchAllDeps as V}from"./launch-all-deps.js";import{setTerminalTitle as j}from"./terminal-title.js";import{initConsolePrefix as K,consolePrefix as r}from"./console-prefix.js";import{initDebugFromArgv as Y}from"./debug.js";import{fetchLatestBorgmcpVersion as q,compareVersionsForStaleness as z}from"./stale-version-check.js";import{defaultCliChoiceDeps as X,detectCliAvailability as C,installedCliNames as J,parseCliFlag as Q,resolveCliChoice as Z}from"./cli-platform.js";import{getRefreshToken as ee,getIdToken as re}from"./config.js";import{composeGetStarted as oe,shouldShowGetStarted as se}from"./get-started.js";import{prepareCodexRemoteLaunch as te,withCodexCwdArg as ie,defaultCodexRemoteDeps as ae}from"./codex-remote.js";import{findLoadedCodexThread as ne}from"./codex-app-server.js";import{buildAgentKickoffPrompt as ce,recordCodexWakeTarget as le,socketPathFromRemoteArgs as b}from"./codex-launch.js";import{codexBorgSessionConfigArgs as de}from"./launch-gate.js";import{addCodexMcpServer as pe,addCodexSessionStartHook as me,addCodexUserPromptSubmitHook as ue,addMcpServer as fe,addProjectSessionStartHook as ge,addUserPromptSubmitHook as he,isCodexMcpServerConfigured as we,isMcpServerConfigured as xe,removeSessionStartHook as ve}from"./config-utils.js";async function Ce(){Y(process.argv),E(),await K();const c=(async()=>{if(!process.stderr.isTTY)return;const e=g(),t=await q();if(!t)return;const n=z(e,t);n.stale&&n.message&&process.stderr.write(`${r()}${n.message}
|
|
3
|
+
`)})();if((process.argv[2]==="--help"||process.argv[2]==="-h")&&(process.stdout.write(L(g())),process.exit(0)),process.argv[2]==="setup"){O(process.argv[3])&&(process.stdout.write(F(g())),process.exit(0)),await import("./setup.js");return}if(process.argv[2]==="assimilate"){const e=B(process.argv.slice(3));e.ok||(process.stderr.write(o.red(`${r()}\u25FC borg assimilate: ${e.error}
|
|
4
|
+
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const t=_(),n=await G({role:e.role,flags:e.flags},t);process.exit(n)}if(process.argv[2]==="spawn"){const e=await N();process.exit(e)}if(process.argv[2]==="sync"){const e=H(process.argv.slice(3));e.ok||(process.stderr.write(o.red(`${r()}\u25FC borg sync: ${e.error}
|
|
5
|
+
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const t=await M({},e.options);process.exit(t)}if(process.argv[2]==="launch-all"){const e=W(process.argv.slice(3));e.ok||(process.stderr.write(o.red(`${r()}\u25FC borg launch-all: ${e.error}
|
|
6
|
+
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const t=V(),n=await U(e.args,t);process.exit(n)}if(se(await ee()!==null,await re()!==null)){const e=J(C()).length>0;process.stdout.write(oe(e)),process.exit(0)}const i=Q(process.argv.slice(2));i.error&&(process.stderr.write(o.red(`${r()}\u25FC ${i.error}
|
|
7
|
+
`)),process.stderr.write("Run `borg --help` for usage.\n"),process.exit(1));const k=async e=>{const t=P({input:process.stdin,output:process.stdout});try{return await t.question(e)}finally{t.close()}},s=await Z(i.cli,X(k,()=>process.stdin.isTTY===!0));be();const l=i.rest,a=await R();j(a?{label:a.droneLabel,cubeName:a.name}:null,A(process.cwd()));const $=a&&s==="claude"?`If you haven't yet, arm a persistent Monitor running the command \`borg-inbox-monitor ${I(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 h=s==="codex"?`borg-wake-${y()}`:null;let m,w=[],u={...process.env,BORG_SESSION:"1"},d=null,p=null;if(s==="codex"&&!l.includes("--remote")){console.error(`${r()}${o.gray("\u25FC Starting Codex remote-wake app-server\u2026")}`);const e=await te(ae());e.warning?(console.error(`${r()}${o.yellow(`warning: ${e.warning}`)}`),m="\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."):m="Codex wake-path capability check passed: remote-control socket established for this session.",w=e.args,u={...process.env,...e.env,BORG_SESSION:"1"},d=b(e.args),p=e.server?.cleanup??null}else s==="codex"&&l.includes("--remote")&&(m="Codex wake-path capability check: using caller-provided --remote socket; if no wake arrives, run borg:regen manually when returning to the session.",d=b(l),d&&(u={...process.env,BORG_CODEX_REMOTE_WAKE:"1",BORG_SESSION:"1"}));const x=ce({cli:s,codexWakeNonce:h,monitorClause:$,codexWakePathClause:m});let f=[...l,x];s==="codex"&&(f=[...de(),...w,...ie(f,process.cwd())]),console.error(`${r()}${o.blue(`\u25FC Launching ${s==="claude"?"Claude Code":"Codex"}\u2026`)}`);const v=S(s,f,{stdio:"inherit",shell:!1,env:u});s==="codex"&&a&&d&&le({deps:{setCodexWakeTarget:D,findLoadedCodexThread:ne},cubeId:a.cubeId,droneId:a.droneId,socketPath:d,passthroughArgs:l,previewNeedle:h??x.slice(0,120),cwd:process.cwd(),launchedAtSeconds:Math.floor(Date.now()/1e3)}),v.on("error",e=>{if(p)try{p()}catch{}e.code==="ENOENT"?(console.error(`${r()}${o.red(`
|
|
8
|
+
\u25FC Failed to launch ${s}`)}`),console.error(`${r()}${o.gray(`Make sure ${s} is installed.
|
|
9
|
+
`)}`)):console.error(`${r()}${o.red(`
|
|
10
|
+
\u25FC Failed to launch ${s}: ${e.message}
|
|
11
|
+
`)}`),process.exit(1)}),v.on("exit",e=>{if(p)try{p()}catch{}process.exit(e??0)})}function be(){const c=C();if(c.claude)try{xe()||fe(),ge(T(process.cwd())),ve(),he()}catch(i){console.error(`${r()}${o.yellow(`warning: Claude Code integration check failed: ${i?.message??i}`)}`)}if(c.codex)try{we()||pe(),me(),ue()}catch(i){console.error(`${r()}${o.yellow(`warning: Codex integration check failed: ${i?.message??i}`)}`)}}Ce().catch(c=>{console.error(`${r()}${o.red(`
|
|
12
|
+
\u25FC Error: ${c.message}
|
|
12
13
|
`)}`),process.exit(1)});
|
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,14 @@ 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
|
+
borg launch-all [cube] Launch all drone worktrees of a cube (default: active cube)
|
|
16
17
|
borg --cli claude|codex Choose agent CLI for this project
|
|
17
18
|
borg --version Show installed version
|
|
18
19
|
|
|
19
20
|
All other arguments are passed through to the selected agent CLI.
|
|
20
|
-
`}function
|
|
21
|
+
`}function n(e){return`borg setup (borgmcp ${e}) \u2014 set up OAuth + register the borg MCP server
|
|
21
22
|
|
|
22
23
|
Borg MCP needs Claude Code or Codex installed first.
|
|
23
24
|
|
|
@@ -28,4 +29,4 @@ Usage:
|
|
|
28
29
|
for SSH / headless / container terminals. Alias: --device.
|
|
29
30
|
Auto-detected on SSH/headless; this forces it.
|
|
30
31
|
borg setup --help Show this help
|
|
31
|
-
`}export{r as isHelpFlag,
|
|
32
|
+
`}export{r as isHelpFlag,n as setupHelpText,o as topLevelHelpText};
|
package/dist/cubes.d.ts
CHANGED
|
@@ -63,6 +63,22 @@ export declare function activeCubeWithFreshRegenIdentity(active: ActiveCube, res
|
|
|
63
63
|
*/
|
|
64
64
|
export declare function clearActiveCube(): Promise<void>;
|
|
65
65
|
export declare function getProjectCliPreference(): Promise<BorgCli | null>;
|
|
66
|
+
/**
|
|
67
|
+
* gh#556 Part 2 — like getProjectCliPreference, but keyed on an arbitrary
|
|
68
|
+
* worktree dir (launch-all reads the saved CLI preference for EACH discovered
|
|
69
|
+
* worktree, not just cwd). Returns null if no preference is saved for that path.
|
|
70
|
+
*/
|
|
71
|
+
export declare function getProjectCliPreferenceForPath(dir: string): Promise<BorgCli | null>;
|
|
72
|
+
/**
|
|
73
|
+
* gh#556 Part 2 — returns all persisted project identities from cubes.json.
|
|
74
|
+
* Used by `borg launch-all` to enumerate drones across all known worktrees
|
|
75
|
+
* (scheme-agnostic — covers both old sibling paths and new ~/.borg paths).
|
|
76
|
+
* Returns an empty array if the file is absent or malformed.
|
|
77
|
+
*/
|
|
78
|
+
export declare function readAllProjectIdentities(): Promise<Array<{
|
|
79
|
+
projectPath: string;
|
|
80
|
+
cube: ActiveCube;
|
|
81
|
+
}>>;
|
|
66
82
|
export declare function setProjectCliPreference(cli: BorgCli): Promise<void>;
|
|
67
83
|
export declare function setCodexWakeTarget(cubeId: string, droneId: string, target: Omit<CodexWakeTargetRecord, 'updatedAt'>): Promise<void>;
|
|
68
84
|
export declare function getCodexWakeTarget(cubeId: string, droneId: string): Promise<CodexWakeTargetRecord | null>;
|
package/dist/cubes.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{existsSync as E}from"node:fs";import{mkdir as
|
|
2
|
-
`,{mode:384})}function
|
|
3
|
-
`,{mode:384})}function x(t,e){if(!u.test(t))throw new Error(`Invalid cubeId: ${t}`);if(!u.test(e))throw new Error(`Invalid droneId: ${e}`);return`${t}:${e}`}function
|
|
4
|
-
`,{mode:384})}async function
|
|
1
|
+
import{existsSync as E}from"node:fs";import{mkdir as f,readFile as p,writeFile as y,unlink as m}from"node:fs/promises";import{homedir as C}from"node:os";import{dirname as c,join as o,resolve as j}from"node:path";const a=o(C(),".config","borgmcp"),s=o(a,"cubes.json"),w=o(a,"launch.json"),d=o(a,"codex-wake-targets.json"),I=o(a,"inboxes");function i(t=process.cwd()){let e=j(t);for(;;){if(E(o(e,".git")))return e;const r=c(e);if(r===e)return j(t);e=r}}const u=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function W(t,e){if(!u.test(t))throw new Error(`Invalid cubeId: ${t}`);if(!u.test(e))throw new Error(`Invalid droneId: ${e}`);return o(I,t,`${e}.log`)}function F(t){return t!==null&&typeof t=="object"&&typeof t.projects=="object"&&t.projects!==null&&!Array.isArray(t.projects)}async function l(){let t;try{t=await p(s,"utf8")}catch(r){if(r?.code==="ENOENT")return null;throw r}let e;try{e=JSON.parse(t)}catch{return null}return F(e)?e:null}async function h(t){await f(c(s),{recursive:!0}),await y(s,JSON.stringify(t,null,2)+`
|
|
2
|
+
`,{mode:384})}function N(t){return t!==null&&typeof t=="object"&&typeof t.projects=="object"&&t.projects!==null&&!Array.isArray(t.projects)}async function g(){let t;try{t=await p(w,"utf8")}catch(e){if(e?.code==="ENOENT")return null;throw e}try{const e=JSON.parse(t);return N(e)?e:null}catch{return null}}async function O(t){await f(c(w),{recursive:!0}),await y(w,JSON.stringify(t,null,2)+`
|
|
3
|
+
`,{mode:384})}function x(t,e){if(!u.test(t))throw new Error(`Invalid cubeId: ${t}`);if(!u.test(e))throw new Error(`Invalid droneId: ${e}`);return`${t}:${e}`}function A(t){return t!==null&&typeof t=="object"&&typeof t.targets=="object"&&t.targets!==null&&!Array.isArray(t.targets)}async function b(){let t;try{t=await p(d,"utf8")}catch(e){if(e?.code==="ENOENT")return null;throw e}try{const e=JSON.parse(t);return A(e)?e:null}catch{return null}}async function k(t){await f(c(d),{recursive:!0}),await y(d,JSON.stringify(t,null,2)+`
|
|
4
|
+
`,{mode:384})}async function _(){const t=await l();if(!t)return null;const e=i(),r=t.projects[e];return!r||typeof r.cubeId!="string"||!r.cubeId||typeof r.droneId!="string"||!r.droneId?null:r}async function $(t){const e=await l()??{projects:{}};e.projects[i()]=t,await h(e)}function v(t,e){const r=e.cube?.name??t.name,n=e.drone?.label??t.droneLabel;return r===t.name&&n===t.droneLabel?t:{...t,name:r,droneLabel:n}}async function D(){const t=await l();if(!t)return;const e=i();if(e in t.projects){if(delete t.projects[e],Object.keys(t.projects).length===0){try{await m(s)}catch(r){if(r?.code!=="ENOENT")throw r}return}await h(t)}}async function J(){const t=await g();if(!t)return null;const e=t.projects[i()];return e?.cli==="claude"||e?.cli==="codex"?e.cli:null}async function R(t){const e=await g();if(!e)return null;const r=e.projects[i(t)];return r?.cli==="claude"||r?.cli==="codex"?r.cli:null}async function U(){const t=await l();return t?Object.entries(t.projects).filter(([,e])=>e!==null&&typeof e=="object"&&typeof e.cubeId=="string"&&e.cubeId.length>0&&typeof e.droneId=="string"&&e.droneId.length>0).map(([e,r])=>({projectPath:e,cube:r})):[]}async function B(t){const e=await g()??{projects:{}};e.projects[i()]={cli:t},await O(e)}async function K(t,e,r){const n=await b()??{targets:{}};n.targets[x(t,e)]={...r,updatedAt:new Date().toISOString()},await k(n)}async function X(t,e){const r=await b();if(!r)return null;const n=r.targets[x(t,e)];return!n||typeof n.threadId!="string"||typeof n.socketPath!="string"?null:n}export{v as activeCubeWithFreshRegenIdentity,D as clearActiveCube,i as findProjectRoot,_ as getActiveCube,X as getCodexWakeTarget,J as getProjectCliPreference,R as getProjectCliPreferenceForPath,W as inboxPathForDrone,U as readAllProjectIdentities,$ as setActiveCube,K as setCodexWakeTarget,B as setProjectCliPreference};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LaunchAllArgs } from './parse-launch-all-args.js';
|
|
2
|
+
import type { LaunchAllDeps } from './launch-all-deps.js';
|
|
3
|
+
export interface RunLaunchAllOptions {
|
|
4
|
+
/** Injectable clock/sleep for deterministic reconciliation tests. */
|
|
5
|
+
now?: () => number;
|
|
6
|
+
sleep?: (ms: number) => Promise<void>;
|
|
7
|
+
nowISO?: () => string;
|
|
8
|
+
borgPath?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function runLaunchAll(args: LaunchAllArgs, deps: LaunchAllDeps, opts?: RunLaunchAllOptions): Promise<number>;
|
|
11
|
+
//# sourceMappingURL=launch-all-cmd.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import{discoverDroneCandidates as y}from"./launch-all-discovery.js";import{resolveBorgPath as v}from"./launch-all-command.js";import{sweepStaleLocks as x,isLockLive as I}from"./launch-all-locks.js";import{runTmuxBackend as S}from"./backends/launch-all-tmux.js";import{runWindowsBackend as A}from"./backends/launch-all-windows.js";import{runPastelistBackend as L}from"./backends/launch-all-pastelist.js";const $=`borg launch-all: tmux not found.
|
|
2
|
+
macOS: brew install tmux
|
|
3
|
+
Debian: sudo apt install tmux
|
|
4
|
+
Fedora: sudo dnf install tmux
|
|
5
|
+
`;function T(t){try{return t.runSync("tmux",["-V"]),!0}catch{return!1}}function D(t){try{return/microsoft|wsl/i.test(t.runSync("uname",["-r"]))}catch{return!1}}function N(t){return/^drone-\d+$/i.test(t)||t.toLowerCase()==="drone"}async function C(t,e){if(t.cubeName!==void 0){const s=(await e.readAllProjectIdentities()).filter(l=>l.cube.name===t.cubeName);return s.length===0?{error:`no cube named '${t.cubeName}' found in cubes.json \u2014 has any drone assimilated into it?`}:{cubeId:s[0].cube.cubeId,name:t.cubeName}}const o=await e.getActiveCube();return o?{cubeId:o.cubeId,name:o.name}:{error:"no active cube in this directory; run `borg assimilate` first, or pass a cube name explicitly"}}function O(t,e){const o=t.flags.mode;if(e.platform()==="win32"&&!D(e))return e.stderr(`native Windows is not supported for interactive launch; using pastelist mode instead (WSL + tmux is the recommended Windows path)
|
|
6
|
+
`),{backend:"pastelist"};if(o==="windows")return{backend:"windows"};if(o==="pastelist")return{backend:"pastelist"};const s=T(e);return o==="tmux"?s?{backend:"tmux"}:{hardFail:$}:s?{backend:"tmux"}:(e.stderr($+`Falling back to pastelist mode (paste the commands below).
|
|
7
|
+
`),{backend:"pastelist"})}function W(t,e){return t.flags.noAttach||!e.isTTY()?"none":e.getEnv("TMUX")?"switch":"attach"}function F(t,e){e.stdout(` tmux attach -t ${t} # re-attach later
|
|
8
|
+
`),e.stdout(` tmux list-windows -t ${t} # list all drone windows
|
|
9
|
+
`),e.stdout(` tmux kill-session -t ${t} # stop all drones
|
|
10
|
+
`)}function P(t){return`borg-${t.replace(/[^a-zA-Z0-9_-]/g,"-")}`}async function B(t,e,o,f,s,l=Date.now,d=a=>new Promise(i=>setTimeout(i,a))){const a=new Map;for(const u of s)a.set(u,"unconfirmed");const i=l()+1e4;for(let u=0;u<20&&!(l()>=i);u++){let r;try{r=await t.getRoster(e,o,f)}catch{break}for(const c of r.drones)a.get(c.id)==="unconfirmed"&&c.seen_since===!0&&a.set(c.id,"verified");if([...a.values()].every(c=>c==="verified"))break;await d(500)}return a}async function p(t,e,o={}){const f=o.now??Date.now,s=o.nowISO??(()=>new Date().toISOString()),l=o.borgPath??v(),d=await C(t,e);if("error"in d)return e.stderr(`borg launch-all: ${d.error}
|
|
11
|
+
`),1;const{cubeId:a,name:i}=d;x(e,a,f());const u=await y({targetCubeId:a,only:t.flags.only},e);if(u.length===0)return t.flags.only!==void 0?(e.stdout(`No worktrees matched --only '${t.flags.only}' for cube '${i}'
|
|
12
|
+
`),N(t.flags.only)||e.stderr(`note: --only '${t.flags.only}' is matched best-effort by drone label; role-name matching needs a drone session and is not available here.
|
|
13
|
+
`)):e.stdout(`No worktrees found for cube '${i}' \u2014 have you run \`borg assimilate --worktree\` to create any drone seats?
|
|
14
|
+
`),0;const r=[];for(const n of u){const h=I(e,a,n.worktreeDir,f());if(h.live&&!t.flags.force){e.stderr(`skipping ${n.droneLabel} (${n.worktreeDir}): appears live. Use --force to re-launch.
|
|
15
|
+
`);continue}h.live&&t.flags.force&&e.stderr(`--force: re-launching ${n.droneLabel} (${n.worktreeDir}); the running session's token will be displaced.
|
|
16
|
+
`),r.push(n)}if(r.length===0)return e.stdout(`All ${u.length} drone(s) for cube '${i}' appear live; nothing to launch (use --force to re-launch).
|
|
17
|
+
`),0;if(t.flags.dryRun){e.stdout(`borg launch-all (dry-run): would launch ${r.length} drone(s) for cube '${i}':
|
|
18
|
+
`);for(const n of r)e.stdout(` ${n.droneLabel} ${n.worktreeDir}
|
|
19
|
+
`);return 0}if(r.length>6&&!t.flags.yes&&(await e.prompt(`About to launch ${r.length} drones for cube '${i}'. Continue? [y/N]: `)).trim().toLowerCase()!=="y")return e.stdout(`Aborted.
|
|
20
|
+
`),0;const c=O(t,e);if("hardFail"in c)return e.stderr(c.hardFail),1;const m=P(i),b=s();try{if(c.backend==="tmux"){const n=W(t,e);await S(r,{sessionName:m,borgPath:l,attachMode:n,launchedAtISO:b},e),n==="none"&&(e.isTTY()||e.stderr(`Launching in detached mode \u2014 stdout is non-TTY. Attach manually with: tmux attach -t ${m}
|
|
21
|
+
`),F(m,e))}else if(c.backend==="windows")await A(r,{borgPath:l,platform:e.platform(),launchedAtISO:b},e);else return L(r,l,e),0}catch(n){return e.stderr(`borg launch-all: ${n instanceof Error?n.message:String(n)}
|
|
22
|
+
`),1}const k=r[0].sessionToken,g=r[0].apiUrl;let w=null;k&&g?w=await B(e,k,g,b,r.map(n=>n.droneId),o.now,o.sleep):e.stderr(`roster reconciliation skipped \u2014 no session token available
|
|
23
|
+
`),e.stdout(`
|
|
24
|
+
borg launch-all: launched ${r.length} drones for cube '${i}'
|
|
25
|
+
|
|
26
|
+
`);for(const n of r){const h=w?w.get(n.droneId)==="verified"?"VERIFIED":"unconfirmed (may still be joining)":"launched";e.stdout(` ${n.droneLabel} ${n.worktreeDir} ${h}
|
|
27
|
+
`)}return e.stdout(`
|
|
28
|
+
Attach: tmux attach -t ${m}
|
|
29
|
+
`),0}export{p as runLaunchAll};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The borg binary that invoked launch-all (spec §5.1). `process.argv[1]` is the
|
|
3
|
+
* absolute path to the running borg script — deterministic, independent of $PATH
|
|
4
|
+
* inside the spawned window's shell (npm link / global / local .bin all work).
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveBorgPath(): string;
|
|
7
|
+
/**
|
|
8
|
+
* The shell command run inside each worktree's window/tab.
|
|
9
|
+
* `keepOpenOnFail` wraps a `|| read` pause so a failed assimilate doesn't close
|
|
10
|
+
* the tmux window before the operator reads the error (tmux convenience only;
|
|
11
|
+
* the pastelist backend omits it — the operator owns their own shell).
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildLaunchCommand(worktreeDir: string, borgPath: string, opts?: {
|
|
14
|
+
keepOpenOnFail?: boolean;
|
|
15
|
+
}): string;
|
|
16
|
+
//# sourceMappingURL=launch-all-command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{shellEscape as r}from"./shell-escape.js";function n(){return process.argv[1]}function i(o,s,t={}){const e=`cd ${r(o)} && ${r(s)} assimilate --here`;return t.keepOpenOnFail?`${e} || { echo "borg assimilate failed \u2014 press Enter to close"; read _; }`:e}export{i as buildLaunchCommand,n as resolveBorgPath};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ActiveCube } from './cubes.js';
|
|
2
|
+
/** Subprocess runner — sync, returns stdout, THROWS on non-zero exit or ENOENT. */
|
|
3
|
+
export type RunSyncFn = (cmd: string, args: string[]) => string;
|
|
4
|
+
export interface LaunchAllDeps {
|
|
5
|
+
/** Subprocess — sync, throws on non-zero exit (git, tmux -V, ...). */
|
|
6
|
+
runSync: (cmd: string, args: string[], opts?: {
|
|
7
|
+
cwd?: string;
|
|
8
|
+
}) => string;
|
|
9
|
+
/** Subprocess — sync, returns exit code WITHOUT throwing (tmux has-session). */
|
|
10
|
+
runSyncExitCode: (cmd: string, args: string[]) => number;
|
|
11
|
+
/**
|
|
12
|
+
* Interactive subprocess with INHERITED stdio (terminal handover) — used for
|
|
13
|
+
* `tmux attach-session` / `switch-client`, where a captured stdout would not
|
|
14
|
+
* render the TUI. Spec §10's capture-runSync cannot interactively attach; this
|
|
15
|
+
* seam completes that (real: spawnSync stdio:'inherit').
|
|
16
|
+
*/
|
|
17
|
+
attachInteractive: (cmd: string, args: string[]) => void;
|
|
18
|
+
/** Absolute path of the current working directory. */
|
|
19
|
+
cwd: () => string;
|
|
20
|
+
/** True iff the path exists on disk. */
|
|
21
|
+
pathExists: (p: string) => boolean;
|
|
22
|
+
/** $HOME / os.homedir(). */
|
|
23
|
+
homedir: () => string;
|
|
24
|
+
/** mkdir -p (recursive; no chmod of existing parents). */
|
|
25
|
+
mkdirp: (dir: string) => void;
|
|
26
|
+
/** Read a file; returns null on ENOENT (never throws for absence). */
|
|
27
|
+
readFileOpt: (p: string) => string | null;
|
|
28
|
+
/** Write a file (mode default 0o600). */
|
|
29
|
+
writeFile: (p: string, content: string, mode?: number) => void;
|
|
30
|
+
/** Unlink a file; does NOT throw on ENOENT. */
|
|
31
|
+
unlinkOpt: (p: string) => void;
|
|
32
|
+
/** mtime in ms, or null if absent. */
|
|
33
|
+
statMtime: (p: string) => number | null;
|
|
34
|
+
/** Directory entries, or [] if absent. */
|
|
35
|
+
listDir: (p: string) => string[];
|
|
36
|
+
/** Cached auth (OAuth/user token) for roster reconciliation + role lookup. */
|
|
37
|
+
getCachedAuth: () => Promise<{
|
|
38
|
+
token: string;
|
|
39
|
+
apiUrl: string;
|
|
40
|
+
} | null>;
|
|
41
|
+
/** Roster call (wraps getRoster from remote-client.ts). */
|
|
42
|
+
getRoster: (token: string, apiUrl: string, since?: string) => Promise<{
|
|
43
|
+
drones: Array<{
|
|
44
|
+
id: string;
|
|
45
|
+
seen_since?: boolean;
|
|
46
|
+
}>;
|
|
47
|
+
}>;
|
|
48
|
+
/** getCube for --only tier-2 role-name resolution (best-effort). */
|
|
49
|
+
getCube: (apiUrl: string, token: string, cubeId: string) => Promise<{
|
|
50
|
+
id: string;
|
|
51
|
+
name: string;
|
|
52
|
+
roles: Array<{
|
|
53
|
+
id: string;
|
|
54
|
+
name: string;
|
|
55
|
+
}>;
|
|
56
|
+
}>;
|
|
57
|
+
/** Saved CLI preference for a worktree path (launch.json). */
|
|
58
|
+
getCliPreferenceForPath: (projectPath: string) => Promise<'claude' | 'codex' | null>;
|
|
59
|
+
/** All persisted project identities from cubes.json. */
|
|
60
|
+
readAllProjectIdentities: () => Promise<Array<{
|
|
61
|
+
projectPath: string;
|
|
62
|
+
cube: ActiveCube;
|
|
63
|
+
}>>;
|
|
64
|
+
/** findProjectRoot (cubes.ts export). */
|
|
65
|
+
findProjectRoot: (dir: string) => string;
|
|
66
|
+
/** Active cube for the cwd (cubes.ts getActiveCube), null if none. */
|
|
67
|
+
getActiveCube: () => Promise<ActiveCube | null>;
|
|
68
|
+
/** Interactive confirmation prompt. */
|
|
69
|
+
prompt: (message: string) => Promise<string>;
|
|
70
|
+
/** TTY check (stdin). */
|
|
71
|
+
isTTY: () => boolean;
|
|
72
|
+
/** Environment variable accessor (e.g. $BORG_TERMINAL, $TMUX). */
|
|
73
|
+
getEnv: (name: string) => string | undefined;
|
|
74
|
+
/** process.platform (injectable for native-Windows/WSL backend-selection tests). */
|
|
75
|
+
platform: () => NodeJS.Platform;
|
|
76
|
+
/** stderr writer. */
|
|
77
|
+
stderr: (line: string) => void;
|
|
78
|
+
/** stdout writer. */
|
|
79
|
+
stdout: (line: string) => void;
|
|
80
|
+
}
|
|
81
|
+
/** Real-IO factory wiring production modules (spec §10). Test code stubs LaunchAllDeps directly. */
|
|
82
|
+
export declare function buildDefaultLaunchAllDeps(): LaunchAllDeps;
|
|
83
|
+
//# sourceMappingURL=launch-all-deps.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{spawnSync as o}from"node:child_process";import{existsSync as n,mkdirSync as s,readFileSync as c,writeFileSync as u,unlinkSync as a,statSync as d,readdirSync as l}from"node:fs";import{homedir as p}from"node:os";import{createInterface as m}from"node:readline/promises";import{readAllProjectIdentities as f,getProjectCliPreferenceForPath as y,findProjectRoot as h,getActiveCube as g}from"./cubes.js";import{getRoster as w,getCube as S,getValidToken as P,API_URL as A}from"./remote-client.js";function I(){return{runSync:(t,r,e)=>{const i=o(t,r,{encoding:"utf-8",cwd:e?.cwd});if(i.error)throw i.error;if(i.status!==0)throw new Error(`${t} exited ${i.status}: ${(i.stderr??"").toString().trim()}`);return(i.stdout??"").toString()},runSyncExitCode:(t,r)=>o(t,r,{encoding:"utf-8"}).status??1,attachInteractive:(t,r)=>{o(t,r,{stdio:"inherit"})},cwd:()=>process.cwd(),pathExists:t=>n(t),homedir:()=>p(),mkdirp:t=>{s(t,{recursive:!0})},readFileOpt:t=>{try{return c(t,"utf-8")}catch{return null}},writeFile:(t,r,e)=>{u(t,r,{mode:e??384})},unlinkOpt:t=>{try{a(t)}catch{}},statMtime:t=>{try{return d(t).mtimeMs}catch{return null}},listDir:t=>{try{return l(t)}catch{return[]}},getCachedAuth:async()=>{try{return{token:await P(),apiUrl:A}}catch{return null}},getRoster:(t,r,e)=>w(t,r,e),getCube:(t,r,e)=>S(e),getCliPreferenceForPath:t=>y(t),readAllProjectIdentities:()=>f(),findProjectRoot:t=>h(t),getActiveCube:()=>g(),prompt:async t=>{const r=m({input:process.stdin,output:process.stdout});try{return(await r.question(t)).trim()}finally{r.close()}},isTTY:()=>process.stdin.isTTY===!0,getEnv:t=>process.env[t],platform:()=>process.platform,stderr:t=>{process.stderr.write(t)},stdout:t=>{process.stdout.write(t)}}}export{I as buildDefaultLaunchAllDeps};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { LaunchAllDeps, RunSyncFn } from './launch-all-deps.js';
|
|
2
|
+
export interface DroneCandidate {
|
|
3
|
+
worktreeDir: string;
|
|
4
|
+
cubeId: string;
|
|
5
|
+
droneId: string;
|
|
6
|
+
droneLabel: string;
|
|
7
|
+
sessionToken: string;
|
|
8
|
+
apiUrl: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* --only TIER-1 (local, no server call): exact case-insensitive droneLabel match,
|
|
12
|
+
* OR droneLabel prefix match (`--only drone` matches `drone-1`, `drone-2`, ...).
|
|
13
|
+
* Tier-2 (role-name) matching is best-effort in the orchestrator (spec §8.4).
|
|
14
|
+
*/
|
|
15
|
+
export declare function matchesOnlyLabel(droneLabel: string, only: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Enumerate the LINKED worktree paths from `git worktree list --porcelain`,
|
|
18
|
+
* dropping the main worktree (always block[0]). Throws a user-readable error if
|
|
19
|
+
* the command fails (not inside a git repo).
|
|
20
|
+
*/
|
|
21
|
+
export declare function enumerateLinkedWorktrees(runSync: RunSyncFn): string[];
|
|
22
|
+
export interface DiscoverOpts {
|
|
23
|
+
targetCubeId: string;
|
|
24
|
+
/** --only filter (tier-1 label match applied here). */
|
|
25
|
+
only?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Full discovery pipeline (spec §3.5): enumerate → cubes.json lookup → filter
|
|
29
|
+
* (dir-present / has-entry / cubeId-match / UUID-valid / --only) → candidates in
|
|
30
|
+
* stable porcelain order.
|
|
31
|
+
*/
|
|
32
|
+
export declare function discoverDroneCandidates(opts: DiscoverOpts, deps: LaunchAllDeps): Promise<DroneCandidate[]>;
|
|
33
|
+
//# sourceMappingURL=launch-all-discovery.d.ts.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
const u=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function c(o){return u.test(o)}function d(o,t){const i=o.toLowerCase(),e=t.toLowerCase();return i===e||i.startsWith(e)}function l(o){let t;try{t=o("git",["worktree","list","--porcelain"])}catch(e){throw new Error(`launch-all: git worktree list failed \u2014 must be run from inside a git repository
|
|
2
|
+
(inner: ${e instanceof Error?e.message:String(e)})`)}return t.trim().split(/\n\n+/).slice(1).map(e=>{const s=e.match(/^worktree (.+)$/m);return s?s[1].trim():null}).filter(e=>e!==null)}async function f(o,t){const i=l((r,n)=>t.runSync(r,n)),e=await t.readAllProjectIdentities(),s=new Map(e.map(r=>[r.projectPath,r.cube])),a=[];for(const r of i){if(!t.pathExists(r)){t.stderr(`skipping ${r}: directory not found (orphaned worktree \u2014 run \`git worktree prune\`)
|
|
3
|
+
`);continue}const n=s.get(r);if(n&&n.cubeId===o.targetCubeId){if(!c(n.cubeId)||!c(n.droneId)){t.stderr(`skipping ${r}: cubes.json entry has malformed cubeId/droneId \u2014 re-assimilate to fix
|
|
4
|
+
`);continue}o.only!==void 0&&!d(n.droneLabel,o.only)||a.push({worktreeDir:r,cubeId:n.cubeId,droneId:n.droneId,droneLabel:n.droneLabel,sessionToken:n.sessionToken,apiUrl:n.apiUrl})}}return a}export{f as discoverDroneCandidates,l as enumerateLinkedWorktrees,d as matchesOnlyLabel};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { LaunchAllDeps } from './launch-all-deps.js';
|
|
2
|
+
export declare const LOCK_STALE_MS: number;
|
|
3
|
+
/** SHA-1 hex of the worktree abs path → fixed-length collision-safe filename. */
|
|
4
|
+
export declare function worktreeLockName(absPath: string): string;
|
|
5
|
+
export declare function locksDir(homeDir: string, cubeId: string): string;
|
|
6
|
+
export declare function lockPath(homeDir: string, cubeId: string, absPath: string): string;
|
|
7
|
+
export interface LockMarker {
|
|
8
|
+
launchedAt: string;
|
|
9
|
+
droneLabel: string;
|
|
10
|
+
worktreeDir: string;
|
|
11
|
+
}
|
|
12
|
+
/** Write the launch marker (mkdir -p the cube's locks dir first). Mode 0o600. */
|
|
13
|
+
export declare function writeLockMarker(deps: LaunchAllDeps, cubeId: string, droneLabel: string, worktreeDir: string, launchedAtISO: string): void;
|
|
14
|
+
/** Delete mtime-stale (>5min) `.pid` markers in locks/<cubeId>/ (crash cleanup). */
|
|
15
|
+
export declare function sweepStaleLocks(deps: LaunchAllDeps, cubeId: string, nowMs: number): void;
|
|
16
|
+
/** True iff a fresh (<=5min by its launchedAt content) marker exists for the seat. */
|
|
17
|
+
export declare function isLockLive(deps: LaunchAllDeps, cubeId: string, worktreeDir: string, nowMs: number): {
|
|
18
|
+
live: boolean;
|
|
19
|
+
launchedAt?: string;
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=launch-all-locks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createHash as s}from"node:crypto";import{join as a}from"node:path";const u=300*1e3;function m(t){return s("sha1").update(t,"utf8").digest("hex")}function l(t,r){return a(t,".config","borgmcp","locks",r)}function f(t,r,e){return a(l(t,r),m(e)+".pid")}function k(t,r,e,i,n){t.mkdirp(l(t.homedir(),r));const o={launchedAt:n,droneLabel:e,worktreeDir:i};t.writeFile(f(t.homedir(),r,i),JSON.stringify(o),384)}function d(t,r,e){const i=l(t.homedir(),r);for(const n of t.listDir(i)){if(!n.endsWith(".pid"))continue;const o=a(i,n),c=t.statMtime(o);c!==null&&e-c>u&&t.unlinkOpt(o)}}function x(t,r,e,i){const n=t.readFileOpt(f(t.homedir(),r,e));if(n===null)return{live:!1};try{const o=JSON.parse(n),c=Date.parse(o.launchedAt);return Number.isFinite(c)?{live:i-c<=u,launchedAt:o.launchedAt}:{live:!1}}catch{return{live:!1}}}export{u as LOCK_STALE_MS,x as isLockLive,f as lockPath,l as locksDir,d as sweepStaleLocks,m as worktreeLockName,k as writeLockMarker};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface LaunchAllFlags {
|
|
2
|
+
mode?: 'tmux' | 'windows' | 'pastelist';
|
|
3
|
+
only?: string;
|
|
4
|
+
dryRun?: boolean;
|
|
5
|
+
cli?: 'claude' | 'codex';
|
|
6
|
+
noAttach?: boolean;
|
|
7
|
+
yes?: boolean;
|
|
8
|
+
force?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface LaunchAllArgs {
|
|
11
|
+
cubeName?: string;
|
|
12
|
+
flags: LaunchAllFlags;
|
|
13
|
+
}
|
|
14
|
+
export type ParseLaunchAllResult = {
|
|
15
|
+
ok: true;
|
|
16
|
+
args: LaunchAllArgs;
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
error: string;
|
|
20
|
+
};
|
|
21
|
+
export declare function parseLaunchAllArgs(rawArgs: string[]): ParseLaunchAllResult;
|
|
22
|
+
//# sourceMappingURL=parse-launch-all-args.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const s="--mode <tmux|windows|pastelist>, --only <name>, --dry-run, --cli <claude|codex>, --no-attach, --yes/-y, --force";function c(o){const r={};let n;for(let t=0;t<o.length;t++){const a=o[t];switch(a){case"--mode":{const e=o[++t];if(e!=="tmux"&&e!=="windows"&&e!=="pastelist")return{ok:!1,error:`--mode must be one of tmux|windows|pastelist (got: ${e??"<missing>"})`};r.mode=e;break}case"--only":{const e=o[++t];if(e===void 0||e.startsWith("--"))return{ok:!1,error:"--only requires a value (role name or drone label)"};r.only=e;break}case"--cli":{const e=o[++t];if(e!=="claude"&&e!=="codex")return{ok:!1,error:`--cli must be one of claude|codex (got: ${e??"<missing>"})`};r.cli=e;break}case"--dry-run":r.dryRun=!0;break;case"--no-attach":r.noAttach=!0;break;case"--yes":case"-y":r.yes=!0;break;case"--force":r.force=!0;break;default:if(a.startsWith("-"))return{ok:!1,error:`unknown flag: ${a}. Supported: ${s}`};if(n!==void 0)return{ok:!1,error:`unexpected extra argument: ${a}`};n=a;break}}return{ok:!0,args:{cubeName:n,flags:r}}}export{c as parseLaunchAllArgs};
|
|
@@ -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.20",
|
|
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",
|