@wenrwa/marketplace-cli 0.4.3 → 0.4.5
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/__tests__/bridge-relay-client.test.d.ts +1 -0
- package/dist/__tests__/bridge-relay-client.test.js +283 -0
- package/dist/__tests__/bridge-relay-mode.test.d.ts +1 -0
- package/dist/__tests__/bridge-relay-mode.test.js +33 -0
- package/dist/__tests__/bridge-repo-checkout.test.d.ts +1 -0
- package/dist/__tests__/bridge-repo-checkout.test.js +53 -0
- package/dist/__tests__/bridge-server.test.js +21 -0
- package/dist/bridge/realPty.js +2 -2
- package/dist/bridge/relayClient.d.ts +42 -0
- package/dist/bridge/relayClient.js +154 -0
- package/dist/bridge/repoCheckout.d.ts +24 -0
- package/dist/bridge/repoCheckout.js +34 -0
- package/dist/bridge/server.d.ts +3 -0
- package/dist/bridge/server.js +13 -3
- package/dist/bridge/sessionManager.d.ts +2 -0
- package/dist/commands/bridge.d.ts +15 -0
- package/dist/commands/bridge.js +54 -0
- package/dist/index.js +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const ws_1 = require("ws");
|
|
4
|
+
const relayClient_1 = require("../bridge/relayClient");
|
|
5
|
+
const sessionManager_1 = require("../bridge/sessionManager");
|
|
6
|
+
// ── Fake PTY (no real shells in CI) ──────────────────────────────────
|
|
7
|
+
class FakePty {
|
|
8
|
+
written = [];
|
|
9
|
+
resizes = [];
|
|
10
|
+
killed = false;
|
|
11
|
+
dataCb = null;
|
|
12
|
+
write(data) { this.written.push(data); }
|
|
13
|
+
resize(cols, rows) { this.resizes.push({ cols, rows }); }
|
|
14
|
+
kill() { this.killed = true; }
|
|
15
|
+
onData(cb) { this.dataCb = cb; }
|
|
16
|
+
onExit(_cb) { }
|
|
17
|
+
emitData(d) { this.dataCb?.(d); }
|
|
18
|
+
}
|
|
19
|
+
const makeFactory = () => {
|
|
20
|
+
const ptys = [];
|
|
21
|
+
const factory = {
|
|
22
|
+
spawn: () => {
|
|
23
|
+
const p = new FakePty();
|
|
24
|
+
ptys.push(p);
|
|
25
|
+
return p;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
return { factory, ptys };
|
|
29
|
+
};
|
|
30
|
+
/** Collects parsed frames and lets tests await the next one (mirrors bridge-server.test.ts). */
|
|
31
|
+
function frameQueue(ws) {
|
|
32
|
+
const frames = [];
|
|
33
|
+
const waiters = [];
|
|
34
|
+
ws.on('message', (raw) => {
|
|
35
|
+
const f = JSON.parse(String(raw));
|
|
36
|
+
const w = waiters.shift();
|
|
37
|
+
if (w)
|
|
38
|
+
w(f);
|
|
39
|
+
else
|
|
40
|
+
frames.push(f);
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
next: () => frames.length > 0
|
|
44
|
+
? Promise.resolve(frames.shift())
|
|
45
|
+
: new Promise((resolve) => waiters.push(resolve)),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** A loopback ws server standing in for the platform relay. */
|
|
49
|
+
function startFakeRelay() {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
const wss = new ws_1.WebSocketServer({ port: 0, host: '127.0.0.1' });
|
|
52
|
+
const queue = [];
|
|
53
|
+
const waiters = [];
|
|
54
|
+
wss.on('connection', (ws) => {
|
|
55
|
+
const w = waiters.shift();
|
|
56
|
+
if (w)
|
|
57
|
+
w(ws);
|
|
58
|
+
else
|
|
59
|
+
queue.push(ws);
|
|
60
|
+
});
|
|
61
|
+
wss.on('listening', () => {
|
|
62
|
+
const addr = wss.address();
|
|
63
|
+
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
64
|
+
resolve({
|
|
65
|
+
wss,
|
|
66
|
+
port,
|
|
67
|
+
nextConnection: () => queue.length > 0 ? Promise.resolve(queue.shift()) : new Promise((r) => waiters.push(r)),
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function closeRelay(wss) {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
for (const client of wss.clients)
|
|
75
|
+
client.terminate();
|
|
76
|
+
wss.close((err) => (err ? reject(err) : resolve()));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
describe('relay client — dial-out handshake', () => {
|
|
80
|
+
it('connects to the relay URL and sends the v1 auth frame first', async () => {
|
|
81
|
+
const relay = await startFakeRelay();
|
|
82
|
+
const { factory } = makeFactory();
|
|
83
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
84
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
85
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
86
|
+
relayToken: 'relay-token-abc',
|
|
87
|
+
sessions,
|
|
88
|
+
});
|
|
89
|
+
const serverSideWs = await relay.nextConnection();
|
|
90
|
+
const q = frameQueue(serverSideWs);
|
|
91
|
+
const authFrame = await q.next();
|
|
92
|
+
expect(authFrame).toEqual({ v: 1, relayToken: 'relay-token-abc' });
|
|
93
|
+
handle.close();
|
|
94
|
+
await closeRelay(relay.wss);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe('relay client — frame service through the SessionManager', () => {
|
|
98
|
+
it('open -> opened; input reaches the PTY; PTY data streams back over the single socket', async () => {
|
|
99
|
+
const relay = await startFakeRelay();
|
|
100
|
+
const { factory, ptys } = makeFactory();
|
|
101
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
102
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
103
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
104
|
+
relayToken: 'tok',
|
|
105
|
+
sessions,
|
|
106
|
+
});
|
|
107
|
+
const ws = await relay.nextConnection();
|
|
108
|
+
const q = frameQueue(ws);
|
|
109
|
+
await q.next(); // auth frame
|
|
110
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24 }));
|
|
111
|
+
const opened = await q.next();
|
|
112
|
+
expect(opened.t).toBe('opened');
|
|
113
|
+
expect(opened.sessionId).toBeTruthy();
|
|
114
|
+
ws.send(JSON.stringify({ t: 'input', sessionId: opened.sessionId, data: 'ls\r' }));
|
|
115
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
116
|
+
expect(ptys[0].written).toEqual(['ls\r']);
|
|
117
|
+
ptys[0].emitData('result');
|
|
118
|
+
const data = await q.next();
|
|
119
|
+
expect(data).toMatchObject({ t: 'data', sessionId: opened.sessionId, data: 'result' });
|
|
120
|
+
handle.close();
|
|
121
|
+
await closeRelay(relay.wss);
|
|
122
|
+
});
|
|
123
|
+
it('second attach takes over: prior sink gets takeover, new one gets replay', async () => {
|
|
124
|
+
const relay = await startFakeRelay();
|
|
125
|
+
const { factory, ptys } = makeFactory();
|
|
126
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
127
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
128
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
129
|
+
relayToken: 'tok',
|
|
130
|
+
sessions,
|
|
131
|
+
});
|
|
132
|
+
const ws = await relay.nextConnection();
|
|
133
|
+
const q = frameQueue(ws);
|
|
134
|
+
await q.next(); // auth
|
|
135
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24 }));
|
|
136
|
+
const opened = await q.next();
|
|
137
|
+
ptys[0].emitData('history');
|
|
138
|
+
await q.next(); // 'history' data frame
|
|
139
|
+
ws.send(JSON.stringify({ t: 'attach', sessionId: opened.sessionId }));
|
|
140
|
+
const [takeover, replay] = [await q.next(), await q.next()];
|
|
141
|
+
// Order isn't guaranteed between takeover-to-old-sink and replay-to-new
|
|
142
|
+
// sink over the SAME socket, so match by type.
|
|
143
|
+
const byType = (t) => [takeover, replay].find((f) => f.t === t);
|
|
144
|
+
expect(byType('takeover')).toMatchObject({ t: 'takeover', sessionId: opened.sessionId });
|
|
145
|
+
expect(byType('replay')).toMatchObject({ t: 'replay', sessionId: opened.sessionId, data: 'history' });
|
|
146
|
+
handle.close();
|
|
147
|
+
await closeRelay(relay.wss);
|
|
148
|
+
});
|
|
149
|
+
it('multiplexes multiple concurrent sessions over the one outbound socket', async () => {
|
|
150
|
+
const relay = await startFakeRelay();
|
|
151
|
+
const { factory, ptys } = makeFactory();
|
|
152
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
153
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
154
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
155
|
+
relayToken: 'tok',
|
|
156
|
+
sessions,
|
|
157
|
+
});
|
|
158
|
+
const ws = await relay.nextConnection();
|
|
159
|
+
const q = frameQueue(ws);
|
|
160
|
+
await q.next(); // auth
|
|
161
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24 }));
|
|
162
|
+
const openedA = await q.next();
|
|
163
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24 }));
|
|
164
|
+
const openedB = await q.next();
|
|
165
|
+
expect(openedA.sessionId).not.toBe(openedB.sessionId);
|
|
166
|
+
ws.send(JSON.stringify({ t: 'input', sessionId: openedA.sessionId, data: 'to-a' }));
|
|
167
|
+
ws.send(JSON.stringify({ t: 'input', sessionId: openedB.sessionId, data: 'to-b' }));
|
|
168
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
169
|
+
expect(ptys[0].written).toEqual(['to-a']);
|
|
170
|
+
expect(ptys[1].written).toEqual(['to-b']);
|
|
171
|
+
ws.send(JSON.stringify({ t: 'resize', sessionId: openedB.sessionId, cols: 100, rows: 40 }));
|
|
172
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
173
|
+
expect(ptys[1].resizes).toEqual([{ cols: 100, rows: 40 }]);
|
|
174
|
+
ws.send(JSON.stringify({ t: 'close', sessionId: openedA.sessionId }));
|
|
175
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
176
|
+
expect(ptys[0].killed).toBe(true);
|
|
177
|
+
expect(sessions.list()).toEqual([openedB.sessionId]);
|
|
178
|
+
handle.close();
|
|
179
|
+
await closeRelay(relay.wss);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
describe('relay client — reconnect with backoff + jitter', () => {
|
|
183
|
+
it('on drop, schedules a reconnect using the computed backoff delay (injectable timer)', async () => {
|
|
184
|
+
const relay = await startFakeRelay();
|
|
185
|
+
const { factory } = makeFactory();
|
|
186
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
187
|
+
const scheduled = [];
|
|
188
|
+
const setTimeoutFn = (cb, ms) => {
|
|
189
|
+
scheduled.push({ cb, ms });
|
|
190
|
+
return scheduled.length; // opaque handle
|
|
191
|
+
};
|
|
192
|
+
const clearTimeoutFn = () => { };
|
|
193
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
194
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
195
|
+
relayToken: 'tok',
|
|
196
|
+
sessions,
|
|
197
|
+
backoff: { initialMs: 100, maxMs: 1000, factor: 2, jitter: () => 1 }, // deterministic: full ceiling
|
|
198
|
+
setTimeoutFn,
|
|
199
|
+
clearTimeoutFn,
|
|
200
|
+
});
|
|
201
|
+
const ws = await relay.nextConnection(); // already open by the time 'connection' fires
|
|
202
|
+
// Drop the connection from the relay side.
|
|
203
|
+
ws.terminate();
|
|
204
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
205
|
+
expect(scheduled.length).toBe(1);
|
|
206
|
+
expect(scheduled[0].ms).toBe(100); // attempt 0: 100 * 2^0 * jitter(1) = 100
|
|
207
|
+
handle.close();
|
|
208
|
+
await closeRelay(relay.wss);
|
|
209
|
+
});
|
|
210
|
+
it('backoff grows exponentially per attempt, capped at maxMs', async () => {
|
|
211
|
+
// A relay that never accepts a connection (nothing listens on this port)
|
|
212
|
+
// means every attempt fails before 'open' — so attempt count never
|
|
213
|
+
// resets, and we can observe pure exponential growth without racing a
|
|
214
|
+
// real handshake's success-resets-backoff behavior.
|
|
215
|
+
const probe = await startFakeRelay();
|
|
216
|
+
const deadPort = probe.port;
|
|
217
|
+
await closeRelay(probe.wss);
|
|
218
|
+
const { factory } = makeFactory();
|
|
219
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
220
|
+
const scheduled = [];
|
|
221
|
+
const setTimeoutFn = (cb, ms) => {
|
|
222
|
+
scheduled.push({ cb, ms });
|
|
223
|
+
return scheduled.length;
|
|
224
|
+
};
|
|
225
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
226
|
+
relayUrl: `ws://127.0.0.1:${deadPort}`,
|
|
227
|
+
relayToken: 'tok',
|
|
228
|
+
sessions,
|
|
229
|
+
backoff: { initialMs: 100, maxMs: 350, factor: 2, jitter: () => 1 },
|
|
230
|
+
setTimeoutFn,
|
|
231
|
+
clearTimeoutFn: () => { },
|
|
232
|
+
});
|
|
233
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
234
|
+
expect(scheduled[0]?.ms).toBe(100); // 100 * 2^0
|
|
235
|
+
scheduled[0].cb();
|
|
236
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
237
|
+
expect(scheduled[1]?.ms).toBe(200); // 100 * 2^1
|
|
238
|
+
scheduled[1].cb();
|
|
239
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
240
|
+
expect(scheduled[2]?.ms).toBe(350); // 100 * 2^2 = 400, capped at 350
|
|
241
|
+
handle.close();
|
|
242
|
+
});
|
|
243
|
+
it('reconnects and re-pairs: PTY survives the drop, replay reflects everything that happened while down', async () => {
|
|
244
|
+
const relay = await startFakeRelay();
|
|
245
|
+
const { factory, ptys } = makeFactory();
|
|
246
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
247
|
+
const scheduled = [];
|
|
248
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
249
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
250
|
+
relayToken: 'tok',
|
|
251
|
+
sessions,
|
|
252
|
+
backoff: { initialMs: 1, jitter: () => 0.01 },
|
|
253
|
+
setTimeoutFn: (cb) => {
|
|
254
|
+
scheduled.push(cb);
|
|
255
|
+
return scheduled.length;
|
|
256
|
+
},
|
|
257
|
+
clearTimeoutFn: () => { },
|
|
258
|
+
});
|
|
259
|
+
const ws1 = await relay.nextConnection();
|
|
260
|
+
const q1 = frameQueue(ws1);
|
|
261
|
+
await q1.next(); // auth
|
|
262
|
+
ws1.send(JSON.stringify({ t: 'open', cols: 80, rows: 24 }));
|
|
263
|
+
const opened = await q1.next();
|
|
264
|
+
// Drop the relay connection — the PTY must NOT be killed.
|
|
265
|
+
ws1.terminate();
|
|
266
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
267
|
+
expect(ptys[0].killed).toBe(false);
|
|
268
|
+
expect(sessions.list()).toEqual([opened.sessionId]);
|
|
269
|
+
// Session produces output while the relay link is down.
|
|
270
|
+
ptys[0].emitData('while-down');
|
|
271
|
+
// Fire the scheduled reconnect (simulates backoff timer elapsing).
|
|
272
|
+
expect(scheduled.length).toBe(1);
|
|
273
|
+
scheduled[0]();
|
|
274
|
+
const ws2 = await relay.nextConnection();
|
|
275
|
+
const q2 = frameQueue(ws2);
|
|
276
|
+
await q2.next(); // fresh auth frame on the new socket
|
|
277
|
+
ws2.send(JSON.stringify({ t: 'attach', sessionId: opened.sessionId }));
|
|
278
|
+
const replay = await q2.next();
|
|
279
|
+
expect(replay).toMatchObject({ t: 'replay', sessionId: opened.sessionId, data: 'while-down' });
|
|
280
|
+
handle.close();
|
|
281
|
+
await closeRelay(relay.wss);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const bridge_1 = require("../commands/bridge");
|
|
4
|
+
describe('resolveRelayMode — CLI/env wiring for `wen-market bridge --relay`', () => {
|
|
5
|
+
it('no --relay flag and no RELAY_URL env → local mode', () => {
|
|
6
|
+
expect((0, bridge_1.resolveRelayMode)({}, {})).toEqual({ mode: 'local' });
|
|
7
|
+
});
|
|
8
|
+
it('--relay without --relay-token → a clear startup error, no env fallback', () => {
|
|
9
|
+
const result = (0, bridge_1.resolveRelayMode)({ relay: 'wss://relay.hiveop.io/agent' }, {});
|
|
10
|
+
expect(result.mode).toBe('relay-error');
|
|
11
|
+
expect(result.message).toMatch(/relay-token/i);
|
|
12
|
+
});
|
|
13
|
+
it('--relay with --relay-token → relay mode with both values', () => {
|
|
14
|
+
const result = (0, bridge_1.resolveRelayMode)({ relay: 'wss://relay.hiveop.io/agent', relayToken: 'rt_abc123' }, {});
|
|
15
|
+
expect(result).toEqual({
|
|
16
|
+
mode: 'relay',
|
|
17
|
+
relayUrl: 'wss://relay.hiveop.io/agent',
|
|
18
|
+
relayToken: 'rt_abc123',
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
it('RELAY_URL + RELAY_TOKEN env vars (container-friendly) → relay mode, no flags needed', () => {
|
|
22
|
+
const result = (0, bridge_1.resolveRelayMode)({}, { RELAY_URL: 'wss://relay.internal/x', RELAY_TOKEN: 'rt_env' });
|
|
23
|
+
expect(result).toEqual({ mode: 'relay', relayUrl: 'wss://relay.internal/x', relayToken: 'rt_env' });
|
|
24
|
+
});
|
|
25
|
+
it('RELAY_URL env with no token anywhere → clear startup error', () => {
|
|
26
|
+
const result = (0, bridge_1.resolveRelayMode)({}, { RELAY_URL: 'wss://relay.internal/x' });
|
|
27
|
+
expect(result.mode).toBe('relay-error');
|
|
28
|
+
});
|
|
29
|
+
it('flag values win over env vars when both present', () => {
|
|
30
|
+
const result = (0, bridge_1.resolveRelayMode)({ relay: 'wss://flag-wins/', relayToken: 'flag-token' }, { RELAY_URL: 'wss://env-loses/', RELAY_TOKEN: 'env-token' });
|
|
31
|
+
expect(result).toEqual({ mode: 'relay', relayUrl: 'wss://flag-wins/', relayToken: 'flag-token' });
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const repoCheckout_1 = require("../bridge/repoCheckout");
|
|
4
|
+
const BASE = '/Users/t/wenrwa-workspaces';
|
|
5
|
+
const makeDeps = (existing = [], cloneFails = false) => {
|
|
6
|
+
const runs = [];
|
|
7
|
+
return {
|
|
8
|
+
runs,
|
|
9
|
+
deps: {
|
|
10
|
+
baseDir: BASE,
|
|
11
|
+
exists: (p) => existing.includes(p),
|
|
12
|
+
mkdir: (_p) => { },
|
|
13
|
+
run: async (cmd, args) => {
|
|
14
|
+
runs.push([cmd, ...args]);
|
|
15
|
+
if (cloneFails)
|
|
16
|
+
throw new Error('clone failed');
|
|
17
|
+
return { stdout: '', code: 0 };
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
describe('parseRepoFullName', () => {
|
|
23
|
+
it('handles https, trailing .git, and bare owner/repo', () => {
|
|
24
|
+
expect((0, repoCheckout_1.parseRepoFullName)('https://github.com/BunnyDAO/CBP')).toBe('BunnyDAO/CBP');
|
|
25
|
+
expect((0, repoCheckout_1.parseRepoFullName)('https://github.com/BunnyDAO/CBP.git')).toBe('BunnyDAO/CBP');
|
|
26
|
+
expect((0, repoCheckout_1.parseRepoFullName)('BunnyDAO/CBP')).toBe('BunnyDAO/CBP');
|
|
27
|
+
expect((0, repoCheckout_1.parseRepoFullName)('not a repo url')).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
describe('ensureRepoCheckout', () => {
|
|
31
|
+
it('reuses an existing checkout without running git', async () => {
|
|
32
|
+
const { deps, runs } = makeDeps([`${BASE}/BunnyDAO/CBP`]);
|
|
33
|
+
const result = await (0, repoCheckout_1.ensureRepoCheckout)('https://github.com/BunnyDAO/CBP', deps);
|
|
34
|
+
expect(result).toEqual({ path: `${BASE}/BunnyDAO/CBP`, action: 'existing' });
|
|
35
|
+
expect(runs).toHaveLength(0);
|
|
36
|
+
});
|
|
37
|
+
it('clones a missing repo into the workspace base dir', async () => {
|
|
38
|
+
const { deps, runs } = makeDeps();
|
|
39
|
+
const result = await (0, repoCheckout_1.ensureRepoCheckout)('https://github.com/BunnyDAO/CBP.git', deps);
|
|
40
|
+
expect(result).toEqual({ path: `${BASE}/BunnyDAO/CBP`, action: 'cloned' });
|
|
41
|
+
expect(runs[0].join(' ')).toBe(`git clone https://github.com/BunnyDAO/CBP.git ${BASE}/BunnyDAO/CBP`);
|
|
42
|
+
});
|
|
43
|
+
it('reports failure without throwing when the clone fails (no local git auth, etc.)', async () => {
|
|
44
|
+
const { deps } = makeDeps([], true);
|
|
45
|
+
const result = await (0, repoCheckout_1.ensureRepoCheckout)('https://github.com/Private/Repo', deps);
|
|
46
|
+
expect(result.action).toBe('failed');
|
|
47
|
+
});
|
|
48
|
+
it('rejects unparseable repo urls', async () => {
|
|
49
|
+
const { deps } = makeDeps();
|
|
50
|
+
const result = await (0, repoCheckout_1.ensureRepoCheckout)('garbage', deps);
|
|
51
|
+
expect(result.action).toBe('failed');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -140,6 +140,27 @@ describe('bridge WebSocket server', () => {
|
|
|
140
140
|
});
|
|
141
141
|
expect(bad.status).toBe(403);
|
|
142
142
|
});
|
|
143
|
+
it('threads repoUrl through resolveCwd into the PTY spawn and the opened frame', async () => {
|
|
144
|
+
await handle.close();
|
|
145
|
+
const cwds = [];
|
|
146
|
+
const factory = { spawn: (o) => { cwds.push(o.cwd); const p = new FakePty(); ptys.push(p); return p; } };
|
|
147
|
+
handle = await (0, server_1.startBridgeServer)({
|
|
148
|
+
sessionToken: TOKEN,
|
|
149
|
+
allowedOrigin: ORIGIN,
|
|
150
|
+
sessions: new sessionManager_1.SessionManager(factory),
|
|
151
|
+
resolveCwd: async (repoUrl) => (repoUrl.includes('CBP') ? '/tmp/wenrwa-workspaces/BunnyDAO/CBP' : null),
|
|
152
|
+
});
|
|
153
|
+
const { ws, q } = await openAndAuth(handle.port);
|
|
154
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24, repoUrl: 'https://github.com/BunnyDAO/CBP' }));
|
|
155
|
+
const opened = await q.next();
|
|
156
|
+
expect(opened.cwd).toBe('/tmp/wenrwa-workspaces/BunnyDAO/CBP');
|
|
157
|
+
expect(cwds[0]).toBe('/tmp/wenrwa-workspaces/BunnyDAO/CBP');
|
|
158
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24, repoUrl: 'https://github.com/Other/Repo' }));
|
|
159
|
+
const fallback = await q.next();
|
|
160
|
+
expect(fallback.cwd).toBeUndefined();
|
|
161
|
+
expect(fallback.note).toContain('home directory');
|
|
162
|
+
ws.close();
|
|
163
|
+
});
|
|
143
164
|
it('unauthenticated frames are rejected', async () => {
|
|
144
165
|
const ws = connect(handle.port);
|
|
145
166
|
await new Promise((r) => ws.on('open', r));
|
package/dist/bridge/realPty.js
CHANGED
|
@@ -68,12 +68,12 @@ function realPtyFactory() {
|
|
|
68
68
|
const pty = require('node-pty');
|
|
69
69
|
const shell = process.env.SHELL || (os.platform() === 'win32' ? 'powershell.exe' : '/bin/bash');
|
|
70
70
|
return {
|
|
71
|
-
spawn: ({ cols, rows }) => {
|
|
71
|
+
spawn: ({ cols, rows, cwd }) => {
|
|
72
72
|
const proc = pty.spawn(shell, [], {
|
|
73
73
|
name: 'xterm-256color',
|
|
74
74
|
cols,
|
|
75
75
|
rows,
|
|
76
|
-
cwd: os.homedir(),
|
|
76
|
+
cwd: cwd || os.homedir(),
|
|
77
77
|
env: process.env,
|
|
78
78
|
});
|
|
79
79
|
return {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { SessionManager } from './sessionManager';
|
|
2
|
+
/** Minimal surface relayClient needs from a socket — lets tests inject fakes if ever needed. */
|
|
3
|
+
export interface WebSocketLike {
|
|
4
|
+
readyState: number;
|
|
5
|
+
send(data: string): void;
|
|
6
|
+
close(code?: number, reason?: string): void;
|
|
7
|
+
on(event: 'open' | 'message' | 'close' | 'error', cb: (...args: any[]) => void): void;
|
|
8
|
+
}
|
|
9
|
+
export interface BackoffOpts {
|
|
10
|
+
/** First retry delay in ms (before jitter). Default 250. */
|
|
11
|
+
initialMs?: number;
|
|
12
|
+
/** Cap on the backoff ceiling. Default 10_000. */
|
|
13
|
+
maxMs?: number;
|
|
14
|
+
/** Multiplier per attempt. Default 2. */
|
|
15
|
+
factor?: number;
|
|
16
|
+
/** Returns a value in [0, 1); overridable for deterministic tests. Default Math.random. */
|
|
17
|
+
jitter?: () => number;
|
|
18
|
+
}
|
|
19
|
+
export interface RelayClientOpts {
|
|
20
|
+
relayUrl: string;
|
|
21
|
+
relayToken: string;
|
|
22
|
+
sessions: SessionManager;
|
|
23
|
+
/** Injectable for tests: defaults to `new WebSocket(url)` from `ws`. */
|
|
24
|
+
wsFactory?: (url: string) => WebSocketLike;
|
|
25
|
+
backoff?: BackoffOpts;
|
|
26
|
+
/** Injectable timer so backoff tests never wait on a real clock. */
|
|
27
|
+
setTimeoutFn?: (cb: () => void, ms: number) => unknown;
|
|
28
|
+
clearTimeoutFn?: (handle: unknown) => void;
|
|
29
|
+
/** Test/observability hooks. */
|
|
30
|
+
onConnect?: () => void;
|
|
31
|
+
onDisconnect?: (info: {
|
|
32
|
+
code?: number;
|
|
33
|
+
reason?: string;
|
|
34
|
+
}) => void;
|
|
35
|
+
onReconnectScheduled?: (delayMs: number) => void;
|
|
36
|
+
}
|
|
37
|
+
export interface RelayClientHandle {
|
|
38
|
+
/** Stop reconnecting and close the current socket. Sessions are untouched — they live in SessionManager. */
|
|
39
|
+
close(): void;
|
|
40
|
+
isConnected(): boolean;
|
|
41
|
+
}
|
|
42
|
+
export declare function startRelayClient(opts: RelayClientOpts): RelayClientHandle;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startRelayClient = startRelayClient;
|
|
4
|
+
/**
|
|
5
|
+
* ht-05 — Terminal Bridge relay mode (dial-out).
|
|
6
|
+
*
|
|
7
|
+
* Instead of listening on localhost for browser connections (server.ts),
|
|
8
|
+
* this DIALS OUT to the platform relay over a single outbound wss
|
|
9
|
+
* connection, authenticates with a per-container relay token, and serves
|
|
10
|
+
* the same v1 frame protocol (see PROTOCOL.md) through the existing
|
|
11
|
+
* SessionManager. Multi-session multiplexing is unchanged: every frame
|
|
12
|
+
* already carries a sessionId, so one outbound socket serves every session
|
|
13
|
+
* exactly like the local server serves them over N inbound sockets.
|
|
14
|
+
*
|
|
15
|
+
* PTYs live in the SessionManager, not the socket — a relay drop never
|
|
16
|
+
* kills a session. This module owns only: dialing out, the auth frame,
|
|
17
|
+
* frame routing through SessionManager, and reconnect-with-backoff. It
|
|
18
|
+
* does no scrollback bookkeeping of its own (SessionManager already does
|
|
19
|
+
* that) — on re-pair the pane sends `attach` again and gets a fresh
|
|
20
|
+
* `replay`, exactly as it would against the local server.
|
|
21
|
+
*/
|
|
22
|
+
const ws_1 = require("ws");
|
|
23
|
+
const PROTOCOL_VERSION = 1;
|
|
24
|
+
function send(ws, frame) {
|
|
25
|
+
if (ws.readyState === ws_1.WebSocket.OPEN)
|
|
26
|
+
ws.send(JSON.stringify(frame));
|
|
27
|
+
}
|
|
28
|
+
function startRelayClient(opts) {
|
|
29
|
+
const backoff = {
|
|
30
|
+
initialMs: opts.backoff?.initialMs ?? 250,
|
|
31
|
+
maxMs: opts.backoff?.maxMs ?? 10_000,
|
|
32
|
+
factor: opts.backoff?.factor ?? 2,
|
|
33
|
+
jitter: opts.backoff?.jitter ?? Math.random,
|
|
34
|
+
};
|
|
35
|
+
const wsFactory = opts.wsFactory ?? ((url) => new ws_1.WebSocket(url));
|
|
36
|
+
const scheduleTimeout = opts.setTimeoutFn ?? ((cb, ms) => setTimeout(cb, ms));
|
|
37
|
+
const cancelTimeout = opts.clearTimeoutFn ?? ((h) => clearTimeout(h));
|
|
38
|
+
let stopped = false;
|
|
39
|
+
let ws = null;
|
|
40
|
+
let connected = false;
|
|
41
|
+
let attempt = 0;
|
|
42
|
+
let reconnectHandle = null;
|
|
43
|
+
function nextDelay() {
|
|
44
|
+
const ceiling = Math.min(backoff.maxMs, backoff.initialMs * Math.pow(backoff.factor, attempt));
|
|
45
|
+
return ceiling * backoff.jitter(); // full jitter: uniform in [0, ceiling]
|
|
46
|
+
}
|
|
47
|
+
function scheduleReconnect() {
|
|
48
|
+
if (stopped)
|
|
49
|
+
return;
|
|
50
|
+
const delay = nextDelay();
|
|
51
|
+
attempt += 1;
|
|
52
|
+
opts.onReconnectScheduled?.(delay);
|
|
53
|
+
reconnectHandle = scheduleTimeout(() => connect(), delay);
|
|
54
|
+
}
|
|
55
|
+
function connect() {
|
|
56
|
+
if (stopped)
|
|
57
|
+
return;
|
|
58
|
+
const socket = wsFactory(opts.relayUrl);
|
|
59
|
+
ws = socket;
|
|
60
|
+
// Per-connection routing state. Recreated on every reconnect; the
|
|
61
|
+
// SessionManager (and its PTYs) is untouched across drops.
|
|
62
|
+
const handles = new Map();
|
|
63
|
+
const attachTo = (sessionId) => {
|
|
64
|
+
const handle = opts.sessions.attach(sessionId, (frame) => {
|
|
65
|
+
if (frame.t === 'data')
|
|
66
|
+
send(socket, { t: 'data', sessionId, data: frame.data });
|
|
67
|
+
else if (frame.t === 'takeover')
|
|
68
|
+
send(socket, { t: 'takeover', sessionId });
|
|
69
|
+
});
|
|
70
|
+
handles.set(sessionId, handle);
|
|
71
|
+
return handle;
|
|
72
|
+
};
|
|
73
|
+
socket.on('open', () => {
|
|
74
|
+
send(socket, { v: PROTOCOL_VERSION, relayToken: opts.relayToken });
|
|
75
|
+
connected = true;
|
|
76
|
+
attempt = 0;
|
|
77
|
+
opts.onConnect?.();
|
|
78
|
+
});
|
|
79
|
+
socket.on('message', (raw) => {
|
|
80
|
+
let frame;
|
|
81
|
+
try {
|
|
82
|
+
frame = JSON.parse(String(raw));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
send(socket, { t: 'error', message: 'invalid frame' });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!frame || !('t' in frame)) {
|
|
89
|
+
send(socket, { t: 'error', message: 'invalid frame' });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
switch (frame.t) {
|
|
94
|
+
case 'open': {
|
|
95
|
+
const sessionId = opts.sessions.open({ cols: frame.cols, rows: frame.rows });
|
|
96
|
+
attachTo(sessionId);
|
|
97
|
+
send(socket, { t: 'opened', sessionId });
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case 'attach': {
|
|
101
|
+
const handle = attachTo(frame.sessionId);
|
|
102
|
+
send(socket, { t: 'replay', sessionId: frame.sessionId, data: handle.replay });
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case 'input':
|
|
106
|
+
handles.get(frame.sessionId)?.write(frame.data);
|
|
107
|
+
break;
|
|
108
|
+
case 'resize':
|
|
109
|
+
handles.get(frame.sessionId)?.resize(frame.cols, frame.rows);
|
|
110
|
+
break;
|
|
111
|
+
case 'detach':
|
|
112
|
+
handles.get(frame.sessionId)?.detach();
|
|
113
|
+
handles.delete(frame.sessionId);
|
|
114
|
+
break;
|
|
115
|
+
case 'close':
|
|
116
|
+
handles.delete(frame.sessionId);
|
|
117
|
+
opts.sessions.close(frame.sessionId);
|
|
118
|
+
break;
|
|
119
|
+
default:
|
|
120
|
+
send(socket, { t: 'error', message: 'unknown frame type' });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
send(socket, { t: 'error', message: err instanceof Error ? err.message : 'relay client error' });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
const onDown = (info) => {
|
|
128
|
+
connected = false;
|
|
129
|
+
// Detach (not close) every live handle: the sessions/PTYs survive the
|
|
130
|
+
// drop in the SessionManager, scrollback keeps accumulating, and the
|
|
131
|
+
// next `attach` after re-pairing gets a fresh replay.
|
|
132
|
+
for (const handle of handles.values())
|
|
133
|
+
handle.detach();
|
|
134
|
+
handles.clear();
|
|
135
|
+
opts.onDisconnect?.(info);
|
|
136
|
+
scheduleReconnect();
|
|
137
|
+
};
|
|
138
|
+
socket.on('close', (code, reason) => onDown({ code, reason: reason ? String(reason) : undefined }));
|
|
139
|
+
// 'ws' always follows a connection-level 'error' with a 'close'; this
|
|
140
|
+
// listener exists only so an unhandled 'error' event never crashes the
|
|
141
|
+
// process (EventEmitter throws on unlistened 'error').
|
|
142
|
+
socket.on('error', () => { });
|
|
143
|
+
}
|
|
144
|
+
connect();
|
|
145
|
+
return {
|
|
146
|
+
close() {
|
|
147
|
+
stopped = true;
|
|
148
|
+
if (reconnectHandle !== null)
|
|
149
|
+
cancelTimeout(reconnectHandle);
|
|
150
|
+
ws?.close();
|
|
151
|
+
},
|
|
152
|
+
isConnected: () => connected,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bct UX round 2 (2026-07-30) — workspace-scoped terminals.
|
|
3
|
+
*
|
|
4
|
+
* The arena's mental model is workspace = repo, so the Terminal pane sends
|
|
5
|
+
* the workspace's GitHub URL with `open`, and the bridge lands the shell in
|
|
6
|
+
* a local checkout of that repo (cloning on first use with the USER'S own
|
|
7
|
+
* git credentials — the platform never touches repo auth). Checkouts live
|
|
8
|
+
* under ~/wenrwa-workspaces/<owner>/<repo>.
|
|
9
|
+
*/
|
|
10
|
+
export interface RepoCheckoutDeps {
|
|
11
|
+
baseDir: string;
|
|
12
|
+
exists: (path: string) => boolean;
|
|
13
|
+
mkdir: (path: string) => void;
|
|
14
|
+
run: (cmd: string, args: string[]) => Promise<{
|
|
15
|
+
stdout: string;
|
|
16
|
+
code: number;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
export interface CheckoutResult {
|
|
20
|
+
path?: string;
|
|
21
|
+
action: 'existing' | 'cloned' | 'failed';
|
|
22
|
+
}
|
|
23
|
+
export declare function parseRepoFullName(repoUrl: string): string | null;
|
|
24
|
+
export declare function ensureRepoCheckout(repoUrl: string, deps: RepoCheckoutDeps): Promise<CheckoutResult>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* bct UX round 2 (2026-07-30) — workspace-scoped terminals.
|
|
4
|
+
*
|
|
5
|
+
* The arena's mental model is workspace = repo, so the Terminal pane sends
|
|
6
|
+
* the workspace's GitHub URL with `open`, and the bridge lands the shell in
|
|
7
|
+
* a local checkout of that repo (cloning on first use with the USER'S own
|
|
8
|
+
* git credentials — the platform never touches repo auth). Checkouts live
|
|
9
|
+
* under ~/wenrwa-workspaces/<owner>/<repo>.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.parseRepoFullName = parseRepoFullName;
|
|
13
|
+
exports.ensureRepoCheckout = ensureRepoCheckout;
|
|
14
|
+
function parseRepoFullName(repoUrl) {
|
|
15
|
+
const m = repoUrl.match(/github\.com[/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?(?:[/?#].*)?$/) ||
|
|
16
|
+
repoUrl.match(/^([\w.-]+)\/([\w.-]+)$/);
|
|
17
|
+
return m ? `${m[1]}/${m[2]}` : null;
|
|
18
|
+
}
|
|
19
|
+
async function ensureRepoCheckout(repoUrl, deps) {
|
|
20
|
+
const fullName = parseRepoFullName(repoUrl);
|
|
21
|
+
if (!fullName)
|
|
22
|
+
return { action: 'failed' };
|
|
23
|
+
const path = `${deps.baseDir}/${fullName}`;
|
|
24
|
+
if (deps.exists(path))
|
|
25
|
+
return { path, action: 'existing' };
|
|
26
|
+
try {
|
|
27
|
+
deps.mkdir(path.slice(0, path.lastIndexOf('/')));
|
|
28
|
+
await deps.run('git', ['clone', repoUrl, path]);
|
|
29
|
+
return { path, action: 'cloned' };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { action: 'failed' };
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/bridge/server.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ export interface BridgeServerOpts {
|
|
|
5
5
|
sessions: SessionManager;
|
|
6
6
|
/** 0 (default) = ephemeral port. */
|
|
7
7
|
port?: number;
|
|
8
|
+
/** Workspace-scoped terminals: resolve a repo URL to a local checkout dir
|
|
9
|
+
* (cloned on first use). Absent/null result → shell opens in $HOME. */
|
|
10
|
+
resolveCwd?: (repoUrl: string) => Promise<string | null>;
|
|
8
11
|
}
|
|
9
12
|
export interface BridgeServerHandle {
|
|
10
13
|
port: number;
|
package/dist/bridge/server.js
CHANGED
|
@@ -137,9 +137,19 @@ function startBridgeServer(opts) {
|
|
|
137
137
|
try {
|
|
138
138
|
switch (frame.t) {
|
|
139
139
|
case 'open': {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
const openSession = (cwd, note) => {
|
|
141
|
+
const sessionId = opts.sessions.open({ cols: frame.cols, rows: frame.rows, cwd });
|
|
142
|
+
attachTo(sessionId);
|
|
143
|
+
send(ws, { t: 'opened', sessionId, ...(cwd ? { cwd } : {}), ...(note ? { note } : {}) });
|
|
144
|
+
};
|
|
145
|
+
if (frame.repoUrl && opts.resolveCwd) {
|
|
146
|
+
opts.resolveCwd(frame.repoUrl)
|
|
147
|
+
.then((cwd) => openSession(cwd ?? undefined, cwd ? undefined : 'repo checkout unavailable — opened in home directory'))
|
|
148
|
+
.catch(() => openSession(undefined, 'repo checkout failed — opened in home directory'));
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
openSession();
|
|
152
|
+
}
|
|
143
153
|
break;
|
|
144
154
|
}
|
|
145
155
|
case 'attach': {
|
|
@@ -9,6 +9,7 @@ export interface PtyFactory {
|
|
|
9
9
|
spawn(opts: {
|
|
10
10
|
cols: number;
|
|
11
11
|
rows: number;
|
|
12
|
+
cwd?: string;
|
|
12
13
|
}): PtyLike;
|
|
13
14
|
}
|
|
14
15
|
export type SinkFrame = {
|
|
@@ -35,6 +36,7 @@ export declare class SessionManager {
|
|
|
35
36
|
open(opts: {
|
|
36
37
|
cols: number;
|
|
37
38
|
rows: number;
|
|
39
|
+
cwd?: string;
|
|
38
40
|
}): string;
|
|
39
41
|
attach(sessionId: string, sink: Sink): AttachmentHandle;
|
|
40
42
|
close(sessionId: string): void;
|
|
@@ -1,3 +1,18 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
export declare const DEFAULT_BRIDGE_PORT = 8377;
|
|
3
|
+
export type RelayCliOpts = {
|
|
4
|
+
relay?: string;
|
|
5
|
+
relayToken?: string;
|
|
6
|
+
};
|
|
7
|
+
export type RelayModeResolution = {
|
|
8
|
+
mode: 'local';
|
|
9
|
+
} | {
|
|
10
|
+
mode: 'relay';
|
|
11
|
+
relayUrl: string;
|
|
12
|
+
relayToken: string;
|
|
13
|
+
} | {
|
|
14
|
+
mode: 'relay-error';
|
|
15
|
+
message: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function resolveRelayMode(opts: RelayCliOpts, env?: Record<string, string | undefined>): RelayModeResolution;
|
|
3
18
|
export declare const bridgeCommand: Command;
|
package/dist/commands/bridge.js
CHANGED
|
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.bridgeCommand = exports.DEFAULT_BRIDGE_PORT = void 0;
|
|
40
|
+
exports.resolveRelayMode = resolveRelayMode;
|
|
40
41
|
/**
|
|
41
42
|
* bct-05 — `wen-market bridge`: the local Terminal Bridge.
|
|
42
43
|
*
|
|
@@ -53,10 +54,12 @@ const commander_1 = require("commander");
|
|
|
53
54
|
const chalk_1 = __importDefault(require("chalk"));
|
|
54
55
|
const sessionManager_1 = require("../bridge/sessionManager");
|
|
55
56
|
const server_1 = require("../bridge/server");
|
|
57
|
+
const relayClient_1 = require("../bridge/relayClient");
|
|
56
58
|
const realPty_1 = require("../bridge/realPty");
|
|
57
59
|
const config_1 = require("../bridge/config");
|
|
58
60
|
const mcpRegister_1 = require("../bridge/mcpRegister");
|
|
59
61
|
const autostart_1 = require("../bridge/autostart");
|
|
62
|
+
const repoCheckout_1 = require("../bridge/repoCheckout");
|
|
60
63
|
const config_2 = require("../utils/config");
|
|
61
64
|
exports.DEFAULT_BRIDGE_PORT = 8377;
|
|
62
65
|
const DEFAULT_ORIGIN = 'https://app.hiveop.io';
|
|
@@ -66,6 +69,19 @@ const DEFAULT_BRIDGE_API_URL = 'https://api.wenrwa.com/api/v1';
|
|
|
66
69
|
function bridgeApiUrl() {
|
|
67
70
|
return process.env.MARKETPLACE_API_URL || (0, config_2.loadConfig)()?.apiUrl || DEFAULT_BRIDGE_API_URL;
|
|
68
71
|
}
|
|
72
|
+
function resolveRelayMode(opts, env = process.env) {
|
|
73
|
+
const relayUrl = opts.relay || env.RELAY_URL;
|
|
74
|
+
if (!relayUrl)
|
|
75
|
+
return { mode: 'local' };
|
|
76
|
+
const relayToken = opts.relayToken || env.RELAY_TOKEN;
|
|
77
|
+
if (!relayToken) {
|
|
78
|
+
return {
|
|
79
|
+
mode: 'relay-error',
|
|
80
|
+
message: 'Relay mode requires a token: pass --relay-token <t> or set RELAY_TOKEN.',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return { mode: 'relay', relayUrl, relayToken };
|
|
84
|
+
}
|
|
69
85
|
const execRunner = (cmd, args) => new Promise((resolve, reject) => {
|
|
70
86
|
(0, child_process_1.execFile)(cmd, args, (err, stdout) => {
|
|
71
87
|
if (err && !stdout)
|
|
@@ -89,7 +105,31 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
89
105
|
.option('--token <connectToken>', 'Connect Token from arena settings (stored on first run, never logged)')
|
|
90
106
|
.option('--port <port>', 'port to listen on (loopback only)', String(exports.DEFAULT_BRIDGE_PORT))
|
|
91
107
|
.option('--origin <origin>', 'allowed browser origin', process.env.WENRWA_BRIDGE_ORIGIN || DEFAULT_ORIGIN)
|
|
108
|
+
.option('--relay <url>', 'ht-05: dial out to this platform relay (wss://…) instead of listening locally — container-agent mode')
|
|
109
|
+
.option('--relay-token <token>', 'relay auth token (or set RELAY_TOKEN) — required with --relay')
|
|
92
110
|
.action(async (opts) => {
|
|
111
|
+
// ht-05: relay mode dials OUT to the platform relay instead of listening
|
|
112
|
+
// on localhost. It skips the Connect Token / MCP registration / discovery
|
|
113
|
+
// file entirely — the container image handles claude/MCP config (ht-06).
|
|
114
|
+
const relayMode = resolveRelayMode(opts);
|
|
115
|
+
if (relayMode.mode === 'relay-error') {
|
|
116
|
+
console.error(chalk_1.default.red(relayMode.message));
|
|
117
|
+
process.exit(1);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (relayMode.mode === 'relay') {
|
|
121
|
+
const sessions = new sessionManager_1.SessionManager((0, realPty_1.realPtyFactory)());
|
|
122
|
+
(0, relayClient_1.startRelayClient)({
|
|
123
|
+
relayUrl: relayMode.relayUrl,
|
|
124
|
+
relayToken: relayMode.relayToken,
|
|
125
|
+
sessions,
|
|
126
|
+
onConnect: () => console.log(chalk_1.default.cyan(`Terminal Bridge dialed in to relay (${relayMode.relayUrl})`)),
|
|
127
|
+
onDisconnect: () => console.log(chalk_1.default.yellow('Relay connection dropped — reconnecting…')),
|
|
128
|
+
});
|
|
129
|
+
console.log(chalk_1.default.dim('Relay mode: serving terminal sessions to the platform relay. Ctrl+C stops the bridge.'));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// ---- local mode (byte-identical to pre-ht-05 behavior) ----
|
|
93
133
|
// 1. Connect Token: flag on first run, config thereafter.
|
|
94
134
|
if (opts.token) {
|
|
95
135
|
if (!opts.token.startsWith('wm_ct_')) {
|
|
@@ -120,11 +160,25 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
120
160
|
console.log(chalk_1.default.yellow('! claude binary not found — bridge will run, but install Claude Code for MCP world-wiring'));
|
|
121
161
|
// 3. Serve.
|
|
122
162
|
const sessionToken = crypto.randomBytes(16).toString('hex');
|
|
163
|
+
const workspacesBase = `${os.homedir()}/wenrwa-workspaces`;
|
|
123
164
|
const handle = await (0, server_1.startBridgeServer)({
|
|
124
165
|
sessionToken,
|
|
125
166
|
allowedOrigin: opts.origin,
|
|
126
167
|
sessions: new sessionManager_1.SessionManager((0, realPty_1.realPtyFactory)()),
|
|
127
168
|
port: Number(opts.port),
|
|
169
|
+
// Workspace-scoped shells: the pane sends the workspace's GitHub URL;
|
|
170
|
+
// we land the PTY in a local checkout (user's own git credentials).
|
|
171
|
+
resolveCwd: async (repoUrl) => {
|
|
172
|
+
const result = await (0, repoCheckout_1.ensureRepoCheckout)(repoUrl, {
|
|
173
|
+
baseDir: workspacesBase,
|
|
174
|
+
exists: (p) => fs.existsSync(p),
|
|
175
|
+
mkdir: (p) => fs.mkdirSync(p, { recursive: true }),
|
|
176
|
+
run: execRunner,
|
|
177
|
+
});
|
|
178
|
+
if (result.action === 'cloned')
|
|
179
|
+
console.log(chalk_1.default.green(`✓ Cloned ${repoUrl} → ${result.path}`));
|
|
180
|
+
return result.path ?? null;
|
|
181
|
+
},
|
|
128
182
|
});
|
|
129
183
|
(0, config_1.writeDiscoveryFile)({ port: handle.port, sessionToken });
|
|
130
184
|
fs.appendFileSync((0, config_1.bridgeLogPath)(), `${new Date().toISOString()} bridge up on ${handle.address}:${handle.port}\n`);
|
package/dist/index.js
CHANGED
|
@@ -15,11 +15,13 @@ const repo_1 = require("./commands/repo");
|
|
|
15
15
|
const prompts_1 = require("./commands/prompts");
|
|
16
16
|
const credits_1 = require("./commands/credits");
|
|
17
17
|
const bridge_1 = require("./commands/bridge");
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
19
|
+
const { version } = require('../package.json');
|
|
18
20
|
const program = new commander_1.Command();
|
|
19
21
|
program
|
|
20
22
|
.name('wen-market')
|
|
21
23
|
.description('Wenrwa Agent Marketplace CLI')
|
|
22
|
-
.version(
|
|
24
|
+
.version(version);
|
|
23
25
|
program.addCommand(auth_1.authCommand);
|
|
24
26
|
program.addCommand(workspace_1.workspaceCommand);
|
|
25
27
|
program.addCommand(bounty_1.bountyCommand);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenrwa/marketplace-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "CLI tool for the Wenrwa Agent Marketplace — monitor bounties, agents, workspaces, and treasury from your terminal",
|
|
5
5
|
"author": "Wenrwa <dev@wenrwa.com>",
|
|
6
6
|
"license": "MIT",
|