borgmcp 1.0.6 → 1.0.8

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.
Files changed (160) hide show
  1. package/README.md +5 -3
  2. package/dist/assimilate-cmd.js +39 -511
  3. package/dist/assimilate-deps.js +3 -177
  4. package/dist/assimilate-welcome.js +2 -24
  5. package/dist/auth-env.js +1 -107
  6. package/dist/auth.js +23 -612
  7. package/dist/claude.js +11 -281
  8. package/dist/cli-help.js +29 -50
  9. package/dist/cli-platform.js +4 -94
  10. package/dist/codex-app-server.js +4 -228
  11. package/dist/codex-app-wake.js +2 -122
  12. package/dist/codex-launch.js +1 -81
  13. package/dist/codex-remote.js +1 -250
  14. package/dist/config-utils.js +3 -385
  15. package/dist/config.js +1 -190
  16. package/dist/console-prefix.js +1 -86
  17. package/dist/cube-name.js +1 -65
  18. package/dist/cubes.js +4 -269
  19. package/dist/debug.js +1 -71
  20. package/dist/device-auth.js +1 -167
  21. package/dist/direct-log.js +1 -11
  22. package/dist/health-beat.js +1 -168
  23. package/dist/inbox-monitor.js +1 -129
  24. package/dist/index.js +26 -1378
  25. package/dist/lifecycle-log-guard.js +2 -93
  26. package/dist/list-roles-render.js +6 -39
  27. package/dist/log-audit.js +3 -186
  28. package/dist/log-stream.js +9 -848
  29. package/dist/name-validator.js +1 -22
  30. package/dist/parse-assimilate-args.js +1 -82
  31. package/dist/postinstall.js +8 -22
  32. package/dist/regen-format.js +11 -337
  33. package/dist/regen.js +5 -83
  34. package/dist/remote-client.d.ts +4 -7
  35. package/dist/remote-client.js +1 -695
  36. package/dist/role-resolver.js +1 -36
  37. package/dist/role-section.js +8 -208
  38. package/dist/roster-render.js +3 -96
  39. package/dist/setup.js +41 -251
  40. package/dist/shell-escape.js +1 -22
  41. package/dist/spawn.js +10 -29
  42. package/dist/stale-version-check.js +1 -102
  43. package/dist/stream-owner.js +2 -202
  44. package/dist/stream-status.js +3 -211
  45. package/dist/subscription-retry.js +1 -23
  46. package/dist/sync-roles-render.js +3 -118
  47. package/dist/sync.js +22 -286
  48. package/dist/templates.js +120 -626
  49. package/dist/terminal-title.js +1 -68
  50. package/dist/token-crypto.js +1 -91
  51. package/dist/token-store.js +1 -222
  52. package/dist/types.d.ts +0 -5
  53. package/dist/types.js +0 -5
  54. package/dist/version.js +2 -78
  55. package/dist/worktree-lifecycle.js +2 -173
  56. package/package.json +12 -2
  57. package/dist/assimilate-cmd.d.ts.map +0 -1
  58. package/dist/assimilate-cmd.js.map +0 -1
  59. package/dist/assimilate-deps.d.ts.map +0 -1
  60. package/dist/assimilate-deps.js.map +0 -1
  61. package/dist/assimilate-welcome.d.ts.map +0 -1
  62. package/dist/assimilate-welcome.js.map +0 -1
  63. package/dist/auth-env.d.ts.map +0 -1
  64. package/dist/auth-env.js.map +0 -1
  65. package/dist/auth.d.ts.map +0 -1
  66. package/dist/auth.js.map +0 -1
  67. package/dist/claude.d.ts.map +0 -1
  68. package/dist/claude.js.map +0 -1
  69. package/dist/cli-help.d.ts.map +0 -1
  70. package/dist/cli-help.js.map +0 -1
  71. package/dist/cli-platform.d.ts.map +0 -1
  72. package/dist/cli-platform.js.map +0 -1
  73. package/dist/codex-app-server.d.ts.map +0 -1
  74. package/dist/codex-app-server.js.map +0 -1
  75. package/dist/codex-app-wake.d.ts.map +0 -1
  76. package/dist/codex-app-wake.js.map +0 -1
  77. package/dist/codex-launch.d.ts.map +0 -1
  78. package/dist/codex-launch.js.map +0 -1
  79. package/dist/codex-remote.d.ts.map +0 -1
  80. package/dist/codex-remote.js.map +0 -1
  81. package/dist/config-utils.d.ts.map +0 -1
  82. package/dist/config-utils.js.map +0 -1
  83. package/dist/config.d.ts.map +0 -1
  84. package/dist/config.js.map +0 -1
  85. package/dist/console-prefix.d.ts.map +0 -1
  86. package/dist/console-prefix.js.map +0 -1
  87. package/dist/cube-name.d.ts.map +0 -1
  88. package/dist/cube-name.js.map +0 -1
  89. package/dist/cubes.d.ts.map +0 -1
  90. package/dist/cubes.js.map +0 -1
  91. package/dist/debug.d.ts.map +0 -1
  92. package/dist/debug.js.map +0 -1
  93. package/dist/device-auth.d.ts.map +0 -1
  94. package/dist/device-auth.js.map +0 -1
  95. package/dist/direct-log.d.ts.map +0 -1
  96. package/dist/direct-log.js.map +0 -1
  97. package/dist/health-beat.d.ts.map +0 -1
  98. package/dist/health-beat.js.map +0 -1
  99. package/dist/inbox-monitor.d.ts.map +0 -1
  100. package/dist/inbox-monitor.js.map +0 -1
  101. package/dist/index.d.ts.map +0 -1
  102. package/dist/index.js.map +0 -1
  103. package/dist/lifecycle-log-guard.d.ts.map +0 -1
  104. package/dist/lifecycle-log-guard.js.map +0 -1
  105. package/dist/list-roles-render.d.ts.map +0 -1
  106. package/dist/list-roles-render.js.map +0 -1
  107. package/dist/log-audit.d.ts.map +0 -1
  108. package/dist/log-audit.js.map +0 -1
  109. package/dist/log-stream.d.ts.map +0 -1
  110. package/dist/log-stream.js.map +0 -1
  111. package/dist/name-validator.d.ts.map +0 -1
  112. package/dist/name-validator.js.map +0 -1
  113. package/dist/parse-assimilate-args.d.ts.map +0 -1
  114. package/dist/parse-assimilate-args.js.map +0 -1
  115. package/dist/postinstall.d.ts.map +0 -1
  116. package/dist/postinstall.js.map +0 -1
  117. package/dist/regen-format.d.ts.map +0 -1
  118. package/dist/regen-format.js.map +0 -1
  119. package/dist/regen.d.ts.map +0 -1
  120. package/dist/regen.js.map +0 -1
  121. package/dist/remote-client.d.ts.map +0 -1
  122. package/dist/remote-client.js.map +0 -1
  123. package/dist/role-resolver.d.ts.map +0 -1
  124. package/dist/role-resolver.js.map +0 -1
  125. package/dist/role-section.d.ts.map +0 -1
  126. package/dist/role-section.js.map +0 -1
  127. package/dist/roster-render.d.ts.map +0 -1
  128. package/dist/roster-render.js.map +0 -1
  129. package/dist/setup.d.ts.map +0 -1
  130. package/dist/setup.js.map +0 -1
  131. package/dist/shell-escape.d.ts.map +0 -1
  132. package/dist/shell-escape.js.map +0 -1
  133. package/dist/spawn.d.ts.map +0 -1
  134. package/dist/spawn.js.map +0 -1
  135. package/dist/stale-version-check.d.ts.map +0 -1
  136. package/dist/stale-version-check.js.map +0 -1
  137. package/dist/stream-owner.d.ts.map +0 -1
  138. package/dist/stream-owner.js.map +0 -1
  139. package/dist/stream-status.d.ts.map +0 -1
  140. package/dist/stream-status.js.map +0 -1
  141. package/dist/subscription-retry.d.ts.map +0 -1
  142. package/dist/subscription-retry.js.map +0 -1
  143. package/dist/sync-roles-render.d.ts.map +0 -1
  144. package/dist/sync-roles-render.js.map +0 -1
  145. package/dist/sync.d.ts.map +0 -1
  146. package/dist/sync.js.map +0 -1
  147. package/dist/templates.d.ts.map +0 -1
  148. package/dist/templates.js.map +0 -1
  149. package/dist/terminal-title.d.ts.map +0 -1
  150. package/dist/terminal-title.js.map +0 -1
  151. package/dist/token-crypto.d.ts.map +0 -1
  152. package/dist/token-crypto.js.map +0 -1
  153. package/dist/token-store.d.ts.map +0 -1
  154. package/dist/token-store.js.map +0 -1
  155. package/dist/types.d.ts.map +0 -1
  156. package/dist/types.js.map +0 -1
  157. package/dist/version.d.ts.map +0 -1
  158. package/dist/version.js.map +0 -1
  159. package/dist/worktree-lifecycle.d.ts.map +0 -1
  160. package/dist/worktree-lifecycle.js.map +0 -1
@@ -1,177 +1,3 @@
1
- /**
2
- * Real-IO factory for the `borg assimilate` orchestrator. Produces a
3
- * fully-wired `AssimilateDeps` whose seams call into the existing
4
- * client modules (remote-client HTTP, cubes.ts persistence, auth.ts
5
- * setup wizard, terminal-title helper).
6
- *
7
- * Test code never calls this — tests construct stub deps directly
8
- * (see `client/__tests__/assimilate-cmd.test.ts:makeStubDeps`).
9
- */
10
- import { spawnSync, spawn as spawnChild } from 'node:child_process';
11
- import { existsSync } from 'node:fs';
12
- import { hostname as osHostname } from 'node:os';
13
- import { createInterface } from 'node:readline/promises';
14
- import { API_URL, getValidToken, listCubes as remoteListCubes, getCube as remoteGetCube, createCube as remoteCreateCube, assimilate as remoteAssimilate, listTemplates as remoteListTemplates, } from './remote-client.js';
15
- import { findProjectRoot as cubesFindProjectRoot, getActiveCube as cubesGetActive, setActiveCube as cubesSetActive, inboxPathForDrone, setCodexWakeTarget, } from './cubes.js';
16
- import { authenticateWithGoogle } from './auth.js';
17
- import { setTerminalTitle as setTitle } from './terminal-title.js';
18
- import { defaultCliChoiceDeps, resolveCliChoice } from './cli-platform.js';
19
- import { prepareCodexRemoteLaunch, defaultCodexRemoteDeps } from './codex-remote.js';
20
- import { findLoadedCodexThread } from './codex-app-server.js';
21
- export function buildDefaultAssimilateDeps() {
22
- return {
23
- runSync: (cmd, args, cwd) => {
24
- const r = spawnSync(cmd, args, { cwd, encoding: 'utf-8' });
25
- return {
26
- status: r.status,
27
- stdout: r.stdout ?? '',
28
- stderr: r.stderr ?? '',
29
- };
30
- },
31
- pathExists: (p) => existsSync(p),
32
- cwd: () => process.cwd(),
33
- chdir: (p) => process.chdir(p),
34
- exec: (cmd, args, cwd, env) => new Promise((resolveExit, rejectExit) => {
35
- const child = spawnChild(cmd, args, {
36
- cwd,
37
- stdio: 'inherit',
38
- shell: false,
39
- env: env ? { ...process.env, ...env } : process.env,
40
- });
41
- child.on('error', rejectExit);
42
- child.on('exit', (code) => resolveExit(code ?? 0));
43
- }),
44
- stderr: (line) => process.stderr.write(line),
45
- stdout: (line) => process.stdout.write(line),
46
- prompt: async (message) => {
47
- const rl = createInterface({ input: process.stdin, output: process.stdout });
48
- try {
49
- return await rl.question(message);
50
- }
51
- finally {
52
- rl.close();
53
- }
54
- },
55
- isTTY: () => process.stdin.isTTY === true,
56
- getHostname: () => osHostname(),
57
- setTerminalTitle: (label, cubeName) => {
58
- setTitle({ label, cubeName }, cubeName);
59
- },
60
- getActiveCube: () => cubesGetActive(),
61
- setActiveCube: (a) => cubesSetActive(a),
62
- findProjectRoot: (cwd) => cubesFindProjectRoot(cwd),
63
- getCachedAuth: async () => {
64
- try {
65
- const token = await getValidToken();
66
- return { token, apiUrl: API_URL };
67
- }
68
- catch {
69
- return null;
70
- }
71
- },
72
- runSetup: async () => {
73
- await authenticateWithGoogle();
74
- const token = await getValidToken();
75
- return { token, apiUrl: API_URL };
76
- },
77
- listCubes: async (_apiUrl, _token) => {
78
- const { cubes } = await remoteListCubes();
79
- return cubes.map((c) => ({ id: c.id, name: c.name }));
80
- },
81
- getCube: async (_apiUrl, _token, cubeId) => {
82
- // BUG-2 fix (v0.9.2): remote-client now unwraps the server's
83
- // `{cube, roles, drones}` shape, so the returned object is
84
- // already flat with id/name/roles at the top level.
85
- const cube = await remoteGetCube(cubeId);
86
- return { id: cube.id, name: cube.name, roles: cube.roles };
87
- },
88
- createCube: async (_apiUrl, _token, params) => {
89
- const cube = await remoteCreateCube(params.name, '', params.template ? { template: params.template } : undefined);
90
- return { id: cube.id, name: cube.name, roles: cube.roles };
91
- },
92
- assimilate: async (_apiUrl, _token, params) => {
93
- const result = await remoteAssimilate({ cube_id: params.cube_id, role_id: params.role_id }, undefined, params.hostname ?? null);
94
- return {
95
- cube_id: result.cube.id,
96
- drone_id: result.drone.id,
97
- drone_label: result.drone.label,
98
- session_token: result.sessionToken,
99
- role_id: result.role.id,
100
- };
101
- },
102
- listTemplates: async (_apiUrl, _token) => {
103
- const { templates } = await remoteListTemplates();
104
- return templates.map((t) => ({ name: t.name, description: t.description }));
105
- },
106
- // CR-PE-F1 wiring (Phase E merge brought this seam in): compute the
107
- // inbox file path used by the borg-inbox-monitor command in step 8
108
- // kickoff. Pure helper from cubes.ts; same computation claude.ts uses.
109
- getInboxPath: (cubeId, droneId) => inboxPathForDrone(cubeId, droneId),
110
- // BUG-5 / v0.9.3 wiring: spawn `borg-mcp` as a stdio child, send
111
- // an initialize request, await a response (or timeout), then kill
112
- // the child. Probe success indicates the MCP server starts cleanly
113
- // and tools/list is reachable; failure means the launched Claude
114
- // session will hit the race and need the kickoff prompt's
115
- // ToolSearch recovery clause.
116
- probeMcpReady: () => new Promise((resolveProbe) => {
117
- const child = spawnChild('borg-mcp', [], { stdio: ['pipe', 'pipe', 'pipe'], shell: false });
118
- let buffer = '';
119
- let settled = false;
120
- const settle = (result) => {
121
- if (settled)
122
- return;
123
- settled = true;
124
- try {
125
- child.kill('SIGTERM');
126
- }
127
- catch { /* ignore */ }
128
- resolveProbe(result);
129
- };
130
- const timeout = setTimeout(() => settle(false), 2000);
131
- child.on('error', () => { clearTimeout(timeout); settle(false); });
132
- child.on('exit', () => { clearTimeout(timeout); settle(settled); });
133
- child.stdout?.on('data', (chunk) => {
134
- buffer += chunk.toString('utf-8');
135
- // initialize response contains "result" with "protocolVersion"
136
- // and "capabilities". Light parse: line-buffered JSON-RPC.
137
- for (const line of buffer.split('\n')) {
138
- if (line.includes('"protocolVersion"') && line.includes('"result"')) {
139
- clearTimeout(timeout);
140
- settle(true);
141
- return;
142
- }
143
- }
144
- });
145
- const initReq = JSON.stringify({
146
- jsonrpc: '2.0',
147
- id: 1,
148
- method: 'initialize',
149
- params: {
150
- protocolVersion: '2024-11-05',
151
- capabilities: {},
152
- clientInfo: { name: 'borg-assimilate-probe', version: '0.9.3' },
153
- },
154
- });
155
- try {
156
- child.stdin?.write(initReq + '\n');
157
- }
158
- catch {
159
- clearTimeout(timeout);
160
- settle(false);
161
- }
162
- }),
163
- resolveCli: (explicit) => resolveCliChoice(explicit, defaultCliChoiceDeps(async (message) => {
164
- const rl = createInterface({ input: process.stdin, output: process.stdout });
165
- try {
166
- return await rl.question(message);
167
- }
168
- finally {
169
- rl.close();
170
- }
171
- }, () => process.stdin.isTTY === true)),
172
- prepareCodexRemoteLaunch: () => prepareCodexRemoteLaunch(defaultCodexRemoteDeps()),
173
- setCodexWakeTarget,
174
- findLoadedCodexThread,
175
- };
176
- }
177
- //# sourceMappingURL=assimilate-deps.js.map
1
+ import{spawnSync as m,spawn as l}from"node:child_process";import{existsSync as f}from"node:fs";import{hostname as b}from"node:os";import{createInterface as u}from"node:readline/promises";import{API_URL as d,getValidToken as p,listCubes as h,getCube as C,createCube as T,assimilate as y,listTemplates as w}from"./remote-client.js";import{findProjectRoot as _,getActiveCube as g,setActiveCube as k,inboxPathForDrone as x,setCodexWakeTarget as v}from"./cubes.js";import{authenticateWithGoogle as A}from"./auth.js";import{setTerminalTitle as R}from"./terminal-title.js";import{defaultCliChoiceDeps as P,resolveCliChoice as S}from"./cli-platform.js";import{prepareCodexRemoteLaunch as U,defaultCodexRemoteDeps as L}from"./codex-remote.js";import{findLoadedCodexThread as D}from"./codex-app-server.js";function z(){return{runSync:(e,s,o)=>{const t=m(e,s,{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),exec:(e,s,o,t)=>new Promise((r,i)=>{const a=l(e,s,{cwd:o,stdio:"inherit",shell:!1,env:t?{...process.env,...t}:process.env});a.on("error",i),a.on("exit",n=>r(n??0))}),stderr:e=>process.stderr.write(e),stdout:e=>process.stdout.write(e),prompt:async e=>{const s=u({input:process.stdin,output:process.stdout});try{return await s.question(e)}finally{s.close()}},isTTY:()=>process.stdin.isTTY===!0,getHostname:()=>b(),setTerminalTitle:(e,s)=>{R({label:e,cubeName:s},s)},getActiveCube:()=>g(),setActiveCube:e=>k(e),findProjectRoot:e=>_(e),getCachedAuth:async()=>{try{return{token:await p(),apiUrl:d}}catch{return null}},runSetup:async()=>(await A(),{token:await p(),apiUrl:d}),listCubes:async(e,s)=>{const{cubes:o}=await h();return o.map(t=>({id:t.id,name:t.name}))},getCube:async(e,s,o)=>{const t=await C(o);return{id:t.id,name:t.name,roles:t.roles}},createCube:async(e,s,o)=>{const t=await T(o.name,"",o.template?{template:o.template}:void 0);return{id:t.id,name:t.name,roles:t.roles}},assimilate:async(e,s,o)=>{const t=await y({cube_id:o.cube_id,role_id:o.role_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}},listTemplates:async(e,s)=>{const{templates:o}=await w();return o.map(t=>({name:t.name,description:t.description}))},getInboxPath:(e,s)=>x(e,s),probeMcpReady:()=>new Promise(e=>{const s=l("borg-mcp",[],{stdio:["pipe","pipe","pipe"],shell:!1});let o="",t=!1;const r=n=>{if(!t){t=!0;try{s.kill("SIGTERM")}catch{}e(n)}},i=setTimeout(()=>r(!1),2e3);s.on("error",()=>{clearTimeout(i),r(!1)}),s.on("exit",()=>{clearTimeout(i),r(t)}),s.stdout?.on("data",n=>{o+=n.toString("utf-8");for(const c of o.split(`
2
+ `))if(c.includes('"protocolVersion"')&&c.includes('"result"')){clearTimeout(i),r(!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{s.stdin?.write(a+`
3
+ `)}catch{clearTimeout(i),r(!1)}}),resolveCli:e=>S(e,P(async s=>{const o=u({input:process.stdin,output:process.stdout});try{return await o.question(s)}finally{o.close()}},()=>process.stdin.isTTY===!0)),prepareCodexRemoteLaunch:()=>U(L()),setCodexWakeTarget:v,findLoadedCodexThread:D}}export{z as buildDefaultAssimilateDeps};
@@ -1,24 +1,2 @@
1
- // Pedagogical hint rendered to stdout immediately after assimilation
2
- // completes, before Claude Code launches. The cube-agnostic shape points
3
- // the user at borg:role / borg:cube / borg:roster / borg:regen /
4
- // borg:read-log rather than embedding role-specific text — so cubes
5
- // using non-default templates (writers-room, ops, etc.) render
6
- // identically to software-dev cubes.
7
- //
8
- // Color: ANSI green on the ✓ glyph only; gated on the caller's useColor
9
- // boolean (computed from process.stdout.isTTY && !NO_COLOR && !CI in the
10
- // assimilate-cmd call site). Body text carries no ANSI by design — color
11
- // is hierarchy-cueing, not decoration.
12
- const GREEN = '\x1b[32m';
13
- const RESET = '\x1b[0m';
14
- export function renderAssimilationWelcome(roleName, cubeName, useColor) {
15
- const check = useColor ? `${GREEN}✓${RESET}` : '✓';
16
- return [
17
- `${check} Joined as \`${roleName}\` in cube \`${cubeName}\`.`,
18
- ``,
19
- `Next: run \`borg:regen\` inside Claude to see your cube.`,
20
- `You're set up — your team can now see you in the cube.`,
21
- ``,
22
- ].join('\n');
23
- }
24
- //# sourceMappingURL=assimilate-welcome.js.map
1
+ const c="\x1B[32m",t="\x1B[0m";function r(e,n,o){return[`${o?`${c}\u2713${t}`:"\u2713"} Joined as \`${e}\` in cube \`${n}\`.`,"","Next: run `borg:regen` inside Claude to see your cube.","You're set up \u2014 your team can now see you in the cube.",""].join(`
2
+ `)}export{r as renderAssimilationWelcome};
package/dist/auth-env.js CHANGED
@@ -1,107 +1 @@
1
- /**
2
- * gh#557 — environment / capability detection for remote-terminal auth.
3
- *
4
- * The default OAuth path (auth.ts) opens a browser and listens on a
5
- * localhost-loopback callback server. Both assumptions break on a remote
6
- * terminal: there's no browser to open, and the loopback URL Google would
7
- * redirect to is unreachable from the user's actual browser on another
8
- * machine. These pure helpers decide when to fall back to the RFC 8628
9
- * device-grant flow (no browser) and when the OS keychain is unavailable
10
- * (so a keychain-less token store must be used instead).
11
- *
12
- * Kept free of side effects (env + platform are injected) so the decision
13
- * logic is unit-testable without a real display / SSH session / keychain.
14
- */
15
- import { AsyncEntry } from '@napi-rs/keyring';
16
- /**
17
- * A BORG_* toggle is "on" only when present and not one of the falsy
18
- * spellings. Mirrors how the rest of the client reads boolean env vars:
19
- * an unset var and the explicit "0"/"false"/"" spellings are all off.
20
- */
21
- function envToggleOn(value) {
22
- if (value === undefined)
23
- return false;
24
- const v = value.trim().toLowerCase();
25
- return v !== '' && v !== '0' && v !== 'false' && v !== 'no';
26
- }
27
- /**
28
- * Decide whether the current environment lacks a usable local browser, so
29
- * the loopback OAuth flow can't work and the device-grant flow should be
30
- * used instead.
31
- *
32
- * Decision order (first match wins):
33
- * 1. BORG_FORCE_BROWSER on → false (escape hatch: operator has an
34
- * SSH-forwarded / X-forwarded browser and wants the loopback flow)
35
- * 2. BORG_NO_BROWSER on → true (explicit opt-in to device flow)
36
- * 3. SSH session → true (remote terminal — even on a Mac the
37
- * browser that opens is on the *server*, unreachable to the user)
38
- * 4. container marker → true (headless by construction)
39
- * 5. Linux without any display (no DISPLAY, no WAYLAND_DISPLAY) → true
40
- * 6. otherwise → false (desktop macOS/Windows, or Linux with
41
- * a display — the loopback flow works)
42
- *
43
- * The `--no-browser` / `--device` CLI flag is surfaced by the caller as
44
- * BORG_NO_BROWSER (or by passing an env with it set) so there's a single
45
- * decision funnel.
46
- */
47
- export function isNoBrowserEnv(probe) {
48
- const env = probe?.env ?? process.env;
49
- const platform = probe?.platform ?? process.platform;
50
- // 1. Explicit force-browser escape hatch wins over every no-browser signal.
51
- if (envToggleOn(env.BORG_FORCE_BROWSER))
52
- return false;
53
- // 2. Explicit opt-in to the no-browser/device flow.
54
- if (envToggleOn(env.BORG_NO_BROWSER))
55
- return true;
56
- // 3. SSH session — the terminal is remote, so any browser we open is on
57
- // the far end and the loopback redirect is unreachable to the user.
58
- if (env.SSH_TTY || env.SSH_CONNECTION || env.SSH_CLIENT)
59
- return true;
60
- // 4. Container (systemd/Docker/podman set `container=`); headless by build.
61
- if (env.container)
62
- return true;
63
- // 5. Headless Linux: no X11 and no Wayland display = no browser to open.
64
- if (platform === 'linux' && !env.DISPLAY && !env.WAYLAND_DISPLAY)
65
- return true;
66
- // 6. Desktop (macOS/Windows) or Linux-with-display: loopback flow is fine.
67
- return false;
68
- }
69
- /**
70
- * The probe round-trip used to decide whether the OS keychain is usable.
71
- * Performs a real set/get/delete against a throwaway account so a missing
72
- * Secret Service (headless Linux), a locked keychain, or any other backend
73
- * failure surfaces as a thrown error rather than a later mid-auth crash.
74
- */
75
- async function defaultKeyringRoundTrip() {
76
- const PROBE_SERVICE = 'borg-mcp';
77
- const PROBE_ACCOUNT = '__borg_keyring_probe__';
78
- const entry = new AsyncEntry(PROBE_SERVICE, PROBE_ACCOUNT);
79
- await entry.setPassword('probe');
80
- await entry.getPassword();
81
- // Best-effort cleanup; a failed delete still proves the keychain works.
82
- try {
83
- await entry.deletePassword();
84
- }
85
- catch {
86
- /* leave the probe entry — its presence is harmless */
87
- }
88
- }
89
- /**
90
- * Returns true when the OS keychain can be written to and read from. The
91
- * round-trip is injectable so callers/tests can supply a deterministic
92
- * probe; in production the default probe touches the real keychain.
93
- *
94
- * Any thrown error from the probe (no Secret Service, locked keychain,
95
- * permission denial) is treated as "unavailable" — the caller then selects
96
- * the encrypted-file fallback store.
97
- */
98
- export async function isKeyringAvailable(roundTrip = defaultKeyringRoundTrip) {
99
- try {
100
- await roundTrip();
101
- return true;
102
- }
103
- catch {
104
- return false;
105
- }
106
- }
107
- //# sourceMappingURL=auth-env.js.map
1
+ import{AsyncEntry as o}from"@napi-rs/keyring";function n(t){if(t===void 0)return!1;const r=t.trim().toLowerCase();return r!==""&&r!=="0"&&r!=="false"&&r!=="no"}function i(t){const r=t?.env??process.env,e=t?.platform??process.platform;return n(r.BORG_FORCE_BROWSER)?!1:!!(n(r.BORG_NO_BROWSER)||r.SSH_TTY||r.SSH_CONNECTION||r.SSH_CLIENT||r.container||e==="linux"&&!r.DISPLAY&&!r.WAYLAND_DISPLAY)}async function s(){const t="borg-mcp",r="__borg_keyring_probe__",e=new o(t,r);await e.setPassword("probe"),await e.getPassword();try{await e.deletePassword()}catch{}}async function u(t=s){try{return await t(),!0}catch{return!1}}export{u as isKeyringAvailable,i as isNoBrowserEnv};