@wenrwa/marketplace-cli 0.4.4 → 0.4.6
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-activity-parser.test.d.ts +1 -0
- package/dist/__tests__/bridge-activity-parser.test.js +0 -0
- package/dist/__tests__/bridge-activity-tap.test.d.ts +1 -0
- package/dist/__tests__/bridge-activity-tap.test.js +178 -0
- package/dist/__tests__/bridge-api-client.test.d.ts +1 -0
- package/dist/__tests__/bridge-api-client.test.js +69 -0
- package/dist/__tests__/bridge-browser-driver.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-driver.test.js +379 -0
- package/dist/__tests__/bridge-browser-frames.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-frames.test.js +166 -0
- package/dist/__tests__/bridge-browser-idle.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-idle.test.js +259 -0
- package/dist/__tests__/bridge-browser-relay.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-relay.test.js +670 -0
- package/dist/__tests__/bridge-bundle-installer.test.d.ts +1 -0
- package/dist/__tests__/bridge-bundle-installer.test.js +126 -0
- package/dist/__tests__/bridge-cdp-client.test.d.ts +1 -0
- package/dist/__tests__/bridge-cdp-client.test.js +133 -0
- package/dist/__tests__/bridge-chromium-launcher.test.d.ts +1 -0
- package/dist/__tests__/bridge-chromium-launcher.test.js +202 -0
- package/dist/__tests__/bridge-command-browser-options.test.d.ts +1 -0
- package/dist/__tests__/bridge-command-browser-options.test.js +37 -0
- package/dist/__tests__/bridge-config-mcp.test.js +4 -0
- package/dist/__tests__/bridge-context-manifest-frame.test.d.ts +1 -0
- package/dist/__tests__/bridge-context-manifest-frame.test.js +125 -0
- package/dist/__tests__/bridge-context-writer.test.d.ts +1 -0
- package/dist/__tests__/bridge-context-writer.test.js +161 -0
- package/dist/__tests__/bridge-device-registration.test.d.ts +1 -0
- package/dist/__tests__/bridge-device-registration.test.js +33 -0
- package/dist/__tests__/bridge-folder-picker-runner.test.d.ts +1 -0
- package/dist/__tests__/bridge-folder-picker-runner.test.js +37 -0
- package/dist/__tests__/bridge-folder-picker.test.d.ts +1 -0
- package/dist/__tests__/bridge-folder-picker.test.js +66 -0
- package/dist/__tests__/bridge-folder-registry.test.d.ts +1 -0
- package/dist/__tests__/bridge-folder-registry.test.js +107 -0
- package/dist/__tests__/bridge-frame-pacer.test.d.ts +1 -0
- package/dist/__tests__/bridge-frame-pacer.test.js +102 -0
- package/dist/__tests__/bridge-install-bundle-frame.test.d.ts +1 -0
- package/dist/__tests__/bridge-install-bundle-frame.test.js +107 -0
- package/dist/__tests__/bridge-local-add-core.test.d.ts +1 -0
- package/dist/__tests__/bridge-local-add-core.test.js +48 -0
- package/dist/__tests__/bridge-local-path-validation-fs.test.d.ts +1 -0
- package/dist/__tests__/bridge-local-path-validation-fs.test.js +88 -0
- package/dist/__tests__/bridge-local-path-validation.test.d.ts +1 -0
- package/dist/__tests__/bridge-local-path-validation.test.js +45 -0
- package/dist/__tests__/bridge-login-status.test.d.ts +1 -0
- package/dist/__tests__/bridge-login-status.test.js +60 -0
- 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-relay-pucc14.test.d.ts +1 -0
- package/dist/__tests__/bridge-relay-pucc14.test.js +230 -0
- package/dist/__tests__/bridge-server-local-endpoints.test.d.ts +1 -0
- package/dist/__tests__/bridge-server-local-endpoints.test.js +197 -0
- package/dist/__tests__/bridge-server.test.js +22 -0
- package/dist/__tests__/bridge-workspace-multirepo.test.d.ts +1 -0
- package/dist/__tests__/bridge-workspace-multirepo.test.js +317 -0
- package/dist/__tests__/devices.test.d.ts +1 -0
- package/dist/__tests__/devices.test.js +66 -0
- package/dist/__tests__/local.test.d.ts +1 -0
- package/dist/__tests__/local.test.js +147 -0
- package/dist/bridge/activityParser.d.ts +53 -0
- package/dist/bridge/activityParser.js +202 -0
- package/dist/bridge/apiClient.d.ts +56 -0
- package/dist/bridge/apiClient.js +50 -0
- package/dist/bridge/browserDriver.d.ts +98 -0
- package/dist/bridge/browserDriver.js +265 -0
- package/dist/bridge/browserFrames.d.ts +182 -0
- package/dist/bridge/browserFrames.js +179 -0
- package/dist/bridge/browserIdle.d.ts +75 -0
- package/dist/bridge/browserIdle.js +120 -0
- package/dist/bridge/bundleInstaller.d.ts +31 -0
- package/dist/bridge/bundleInstaller.js +127 -0
- package/dist/bridge/cdpClient.d.ts +47 -0
- package/dist/bridge/cdpClient.js +102 -0
- package/dist/bridge/chromiumLauncher.d.ts +45 -0
- package/dist/bridge/chromiumLauncher.js +113 -0
- package/dist/bridge/config.d.ts +3 -0
- package/dist/bridge/contextWriter.d.ts +31 -0
- package/dist/bridge/contextWriter.js +139 -0
- package/dist/bridge/deviceRegistration.d.ts +28 -0
- package/dist/bridge/deviceRegistration.js +19 -0
- package/dist/bridge/folderPicker.d.ts +34 -0
- package/dist/bridge/folderPicker.js +36 -0
- package/dist/bridge/folderPickerRunner.d.ts +3 -0
- package/dist/bridge/folderPickerRunner.js +25 -0
- package/dist/bridge/folderRegistry.d.ts +19 -0
- package/dist/bridge/folderRegistry.js +100 -0
- package/dist/bridge/framePacer.d.ts +61 -0
- package/dist/bridge/framePacer.js +79 -0
- package/dist/bridge/localAdd.d.ts +28 -0
- package/dist/bridge/localAdd.js +67 -0
- package/dist/bridge/localPathValidation.d.ts +20 -0
- package/dist/bridge/localPathValidation.js +58 -0
- package/dist/bridge/localPathValidationFs.d.ts +2 -0
- package/dist/bridge/localPathValidationFs.js +98 -0
- package/dist/bridge/loginStatus.d.ts +38 -0
- package/dist/bridge/loginStatus.js +56 -0
- package/dist/bridge/realPty.js +6 -2
- package/dist/bridge/relayClient.d.ts +166 -0
- package/dist/bridge/relayClient.js +517 -0
- package/dist/bridge/repoCheckout.d.ts +31 -0
- package/dist/bridge/repoCheckout.js +18 -0
- package/dist/bridge/server.d.ts +33 -0
- package/dist/bridge/server.js +98 -10
- package/dist/bridge/sessionManager.d.ts +18 -0
- package/dist/bridge/sessionManager.js +15 -1
- package/dist/commands/bridge.d.ts +21 -0
- package/dist/commands/bridge.js +194 -1
- package/dist/commands/devices.d.ts +9 -0
- package/dist/commands/devices.js +53 -0
- package/dist/commands/local.d.ts +9 -0
- package/dist/commands/local.js +141 -0
- package/dist/index.js +7 -1
- package/package.json +1 -1
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
/**
|
|
37
|
+
* pucc-03 — model B multi-repo + per-terminal workspace cwd/env (ADR-0031).
|
|
38
|
+
*
|
|
39
|
+
* One container holds many workspaces' repos under /data/repos/<workspaceId>,
|
|
40
|
+
* cloned on demand the first time a terminal for that workspace opens. Every
|
|
41
|
+
* workspace-scoped terminal carries WENRWA_WORKSPACE_ID (and a per-session
|
|
42
|
+
* HT_AUTONOMOUS) in its PTY env so downstream tooling (pucc-04) can resolve
|
|
43
|
+
* the workspace inside the shared box. This suite pins that contract with the
|
|
44
|
+
* existing faked-PTY / fake-relay harness — no docker, no real shells.
|
|
45
|
+
*/
|
|
46
|
+
const os = __importStar(require("os"));
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const child_process_1 = require("child_process");
|
|
50
|
+
const ws_1 = require("ws");
|
|
51
|
+
const relayClient_1 = require("../bridge/relayClient");
|
|
52
|
+
const sessionManager_1 = require("../bridge/sessionManager");
|
|
53
|
+
const repoCheckout_1 = require("../bridge/repoCheckout");
|
|
54
|
+
class FakePty {
|
|
55
|
+
opts;
|
|
56
|
+
written = [];
|
|
57
|
+
constructor(opts) {
|
|
58
|
+
this.opts = opts;
|
|
59
|
+
}
|
|
60
|
+
write(data) { this.written.push(data); }
|
|
61
|
+
resize() { }
|
|
62
|
+
kill() { }
|
|
63
|
+
dataCb = null;
|
|
64
|
+
onData(cb) { this.dataCb = cb; }
|
|
65
|
+
onExit(_cb) { }
|
|
66
|
+
emitData(d) { this.dataCb?.(d); }
|
|
67
|
+
}
|
|
68
|
+
const makeFactory = () => {
|
|
69
|
+
const ptys = [];
|
|
70
|
+
const factory = {
|
|
71
|
+
spawn: (opts) => {
|
|
72
|
+
const p = new FakePty(opts);
|
|
73
|
+
ptys.push(p);
|
|
74
|
+
return p;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
return { factory, ptys };
|
|
78
|
+
};
|
|
79
|
+
// ── fake-relay + frame-queue helpers (mirror bridge-relay-client.test.ts) ──
|
|
80
|
+
function frameQueue(ws) {
|
|
81
|
+
const frames = [];
|
|
82
|
+
const waiters = [];
|
|
83
|
+
ws.on('message', (raw) => {
|
|
84
|
+
const f = JSON.parse(String(raw));
|
|
85
|
+
const w = waiters.shift();
|
|
86
|
+
if (w)
|
|
87
|
+
w(f);
|
|
88
|
+
else
|
|
89
|
+
frames.push(f);
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
next: () => frames.length > 0 ? Promise.resolve(frames.shift()) : new Promise((r) => waiters.push(r)),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function startFakeRelay() {
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
const wss = new ws_1.WebSocketServer({ port: 0, host: '127.0.0.1' });
|
|
98
|
+
const queue = [];
|
|
99
|
+
const waiters = [];
|
|
100
|
+
wss.on('connection', (ws) => {
|
|
101
|
+
const w = waiters.shift();
|
|
102
|
+
if (w)
|
|
103
|
+
w(ws);
|
|
104
|
+
else
|
|
105
|
+
queue.push(ws);
|
|
106
|
+
});
|
|
107
|
+
wss.on('listening', () => {
|
|
108
|
+
const addr = wss.address();
|
|
109
|
+
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
110
|
+
resolve({
|
|
111
|
+
wss,
|
|
112
|
+
port,
|
|
113
|
+
nextConnection: () => (queue.length > 0 ? Promise.resolve(queue.shift()) : new Promise((r) => waiters.push(r))),
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function closeRelay(wss) {
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
for (const client of wss.clients)
|
|
121
|
+
client.terminate();
|
|
122
|
+
wss.close((err) => (err ? reject(err) : resolve()));
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// ── ensureWorkspaceRepo: /data/repos/<workspaceId> clone-on-demand ──────
|
|
126
|
+
describe('ensureWorkspaceRepo — per-workspace clone-on-demand (ADR-0031 layout)', () => {
|
|
127
|
+
const REPOS = '/data/repos';
|
|
128
|
+
const makeDeps = (existing = [], cloneFails = false) => {
|
|
129
|
+
const runs = [];
|
|
130
|
+
return {
|
|
131
|
+
runs,
|
|
132
|
+
deps: {
|
|
133
|
+
reposBaseDir: REPOS,
|
|
134
|
+
exists: (p) => existing.includes(p),
|
|
135
|
+
mkdir: (_p) => { },
|
|
136
|
+
run: async (cmd, args) => {
|
|
137
|
+
runs.push([cmd, ...args]);
|
|
138
|
+
if (cloneFails)
|
|
139
|
+
throw new Error('clone failed');
|
|
140
|
+
return { stdout: '', code: 0 };
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
it('clones a missing workspace repo to /data/repos/<workspaceId>', async () => {
|
|
146
|
+
const { deps, runs } = makeDeps();
|
|
147
|
+
const result = await (0, repoCheckout_1.ensureWorkspaceRepo)({ workspaceId: 'ws-42', repoUrl: 'https://github.com/acme/widgets.git' }, deps);
|
|
148
|
+
expect(result).toEqual({ path: `${REPOS}/ws-42`, action: 'cloned' });
|
|
149
|
+
expect(runs[0].join(' ')).toBe(`git clone https://github.com/acme/widgets.git ${REPOS}/ws-42`);
|
|
150
|
+
});
|
|
151
|
+
it('reuses an existing checkout (.git present) without running git — no re-clone', async () => {
|
|
152
|
+
const { deps, runs } = makeDeps([`${REPOS}/ws-42/.git`]);
|
|
153
|
+
const result = await (0, repoCheckout_1.ensureWorkspaceRepo)({ workspaceId: 'ws-42', repoUrl: 'https://github.com/acme/widgets.git' }, deps);
|
|
154
|
+
expect(result).toEqual({ path: `${REPOS}/ws-42`, action: 'existing' });
|
|
155
|
+
expect(runs).toHaveLength(0);
|
|
156
|
+
});
|
|
157
|
+
it('is keyed by workspaceId, not repo name (distinct workspaces → distinct dirs)', async () => {
|
|
158
|
+
const { deps } = makeDeps();
|
|
159
|
+
const a = await (0, repoCheckout_1.ensureWorkspaceRepo)({ workspaceId: 'alpha', repoUrl: 'https://github.com/acme/widgets.git' }, deps);
|
|
160
|
+
const b = await (0, repoCheckout_1.ensureWorkspaceRepo)({ workspaceId: 'beta', repoUrl: 'https://github.com/acme/widgets.git' }, deps);
|
|
161
|
+
expect(a.path).toBe(`${REPOS}/alpha`);
|
|
162
|
+
expect(b.path).toBe(`${REPOS}/beta`);
|
|
163
|
+
});
|
|
164
|
+
it('reports failure (→ terminal falls back to home) when there is no url and no checkout', async () => {
|
|
165
|
+
const { deps, runs } = makeDeps();
|
|
166
|
+
const result = await (0, repoCheckout_1.ensureWorkspaceRepo)({ workspaceId: 'ws-42' }, deps);
|
|
167
|
+
expect(result.action).toBe('failed');
|
|
168
|
+
expect(runs).toHaveLength(0);
|
|
169
|
+
});
|
|
170
|
+
it('reports failure without throwing when the clone fails', async () => {
|
|
171
|
+
const { deps } = makeDeps([], true);
|
|
172
|
+
const result = await (0, repoCheckout_1.ensureWorkspaceRepo)({ workspaceId: 'ws-42', repoUrl: 'https://github.com/p/r' }, deps);
|
|
173
|
+
expect(result.action).toBe('failed');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
// ── SessionManager: per-session env reaches the PTY factory ─────────────
|
|
177
|
+
describe('SessionManager — per-session env passthrough (pucc-03)', () => {
|
|
178
|
+
it('forwards env to the PTY factory spawn opts', () => {
|
|
179
|
+
const { factory, ptys } = makeFactory();
|
|
180
|
+
const sm = new sessionManager_1.SessionManager(factory);
|
|
181
|
+
sm.open({ cols: 80, rows: 24, cwd: '/data/repos/ws-1', env: { WENRWA_WORKSPACE_ID: 'ws-1' } });
|
|
182
|
+
expect(ptys[0].opts.cwd).toBe('/data/repos/ws-1');
|
|
183
|
+
expect(ptys[0].opts.env).toEqual({ WENRWA_WORKSPACE_ID: 'ws-1' });
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
// ── relay client: the workspaceId → cwd + env contract pucc-04 builds on ──
|
|
187
|
+
describe('relay client — workspace-scoped open (pucc-03 contract)', () => {
|
|
188
|
+
it('open with workspaceId+repoUrl spawns in the resolved cwd with WENRWA_WORKSPACE_ID env, echoes workspaceId', async () => {
|
|
189
|
+
const relay = await startFakeRelay();
|
|
190
|
+
const { factory, ptys } = makeFactory();
|
|
191
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
192
|
+
const resolveCalls = [];
|
|
193
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
194
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
195
|
+
relayToken: 'tok',
|
|
196
|
+
sessions,
|
|
197
|
+
resolveWorkspaceCwd: async (o) => {
|
|
198
|
+
resolveCalls.push(o);
|
|
199
|
+
return `/data/repos/${o.workspaceId}`;
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
const ws = await relay.nextConnection();
|
|
203
|
+
const q = frameQueue(ws);
|
|
204
|
+
await q.next(); // auth
|
|
205
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24, workspaceId: 'ws-7', repoUrl: 'https://github.com/acme/w.git' }));
|
|
206
|
+
const opened = await q.next();
|
|
207
|
+
expect(opened.t).toBe('opened');
|
|
208
|
+
expect(opened.workspaceId).toBe('ws-7');
|
|
209
|
+
expect(opened.cwd).toBe('/data/repos/ws-7');
|
|
210
|
+
expect(resolveCalls).toEqual([{ workspaceId: 'ws-7', repoUrl: 'https://github.com/acme/w.git' }]);
|
|
211
|
+
expect(ptys[0].opts.cwd).toBe('/data/repos/ws-7');
|
|
212
|
+
expect(ptys[0].opts.env).toMatchObject({ WENRWA_WORKSPACE_ID: 'ws-7', HT_AUTONOMOUS: '0' });
|
|
213
|
+
handle.close();
|
|
214
|
+
await closeRelay(relay.wss);
|
|
215
|
+
});
|
|
216
|
+
it('autonomous:true selects HT_AUTONOMOUS=1 per session (claude --dangerously-skip-permissions)', async () => {
|
|
217
|
+
const relay = await startFakeRelay();
|
|
218
|
+
const { factory, ptys } = makeFactory();
|
|
219
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
220
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
221
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
222
|
+
relayToken: 'tok',
|
|
223
|
+
sessions,
|
|
224
|
+
resolveWorkspaceCwd: async ({ workspaceId }) => `/data/repos/${workspaceId}`,
|
|
225
|
+
});
|
|
226
|
+
const ws = await relay.nextConnection();
|
|
227
|
+
const q = frameQueue(ws);
|
|
228
|
+
await q.next();
|
|
229
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24, workspaceId: 'ws-9', autonomous: true }));
|
|
230
|
+
await q.next();
|
|
231
|
+
expect(ptys[0].opts.env).toMatchObject({ WENRWA_WORKSPACE_ID: 'ws-9', HT_AUTONOMOUS: '1' });
|
|
232
|
+
handle.close();
|
|
233
|
+
await closeRelay(relay.wss);
|
|
234
|
+
});
|
|
235
|
+
it('sets WENRWA_WORKSPACE_ID even when the checkout is unavailable (cwd falls back to home)', async () => {
|
|
236
|
+
const relay = await startFakeRelay();
|
|
237
|
+
const { factory, ptys } = makeFactory();
|
|
238
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
239
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
240
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
241
|
+
relayToken: 'tok',
|
|
242
|
+
sessions,
|
|
243
|
+
resolveWorkspaceCwd: async () => null, // clone failed / no url
|
|
244
|
+
});
|
|
245
|
+
const ws = await relay.nextConnection();
|
|
246
|
+
const q = frameQueue(ws);
|
|
247
|
+
await q.next();
|
|
248
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24, workspaceId: 'ws-x' }));
|
|
249
|
+
const opened = await q.next();
|
|
250
|
+
expect(opened.workspaceId).toBe('ws-x');
|
|
251
|
+
expect(opened.cwd).toBeUndefined();
|
|
252
|
+
expect(ptys[0].opts.cwd).toBeUndefined();
|
|
253
|
+
expect(ptys[0].opts.env).toMatchObject({ WENRWA_WORKSPACE_ID: 'ws-x' });
|
|
254
|
+
handle.close();
|
|
255
|
+
await closeRelay(relay.wss);
|
|
256
|
+
});
|
|
257
|
+
it('legacy fallback: open WITHOUT workspaceId spawns a plain shell (no cwd, no injected env)', async () => {
|
|
258
|
+
const relay = await startFakeRelay();
|
|
259
|
+
const { factory, ptys } = makeFactory();
|
|
260
|
+
const sessions = new sessionManager_1.SessionManager(factory);
|
|
261
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
262
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
263
|
+
relayToken: 'tok',
|
|
264
|
+
sessions,
|
|
265
|
+
resolveWorkspaceCwd: async () => '/should/not/be/used',
|
|
266
|
+
});
|
|
267
|
+
const ws = await relay.nextConnection();
|
|
268
|
+
const q = frameQueue(ws);
|
|
269
|
+
await q.next();
|
|
270
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24 }));
|
|
271
|
+
const opened = await q.next();
|
|
272
|
+
expect(opened.t).toBe('opened');
|
|
273
|
+
expect(opened.workspaceId).toBeUndefined();
|
|
274
|
+
expect(ptys[0].opts.cwd).toBeUndefined();
|
|
275
|
+
expect(ptys[0].opts.env).toBeUndefined();
|
|
276
|
+
handle.close();
|
|
277
|
+
await closeRelay(relay.wss);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
/**
|
|
281
|
+
* Env-gated integration (RUN_HT_INTEGRATION): proves clone-on-demand + reuse
|
|
282
|
+
* against a REAL git binary and temp dirs — no docker required, but skipped by
|
|
283
|
+
* default so CI/this-machine never blocks on it. The full docker-level check
|
|
284
|
+
* (RUN_HT_DOCKER) lands in the deploy/verify stage (pucc-09 cutover), where an
|
|
285
|
+
* actual image + Fly volume exist to exercise the shared-container path.
|
|
286
|
+
*/
|
|
287
|
+
const runIntegration = process.env.RUN_HT_INTEGRATION === '1';
|
|
288
|
+
(runIntegration ? describe : describe.skip)('ensureWorkspaceRepo — real git clone-on-demand (env-gated)', () => {
|
|
289
|
+
it('clones on first call then reuses the checkout on the second (idempotent)', async () => {
|
|
290
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pucc03-'));
|
|
291
|
+
const origin = path.join(root, 'origin.git');
|
|
292
|
+
const repos = path.join(root, 'repos');
|
|
293
|
+
(0, child_process_1.execFileSync)('git', ['init', '--bare', origin]);
|
|
294
|
+
// seed the bare origin with one commit so `git clone` produces a checkout
|
|
295
|
+
const seed = path.join(root, 'seed');
|
|
296
|
+
(0, child_process_1.execFileSync)('git', ['clone', origin, seed]);
|
|
297
|
+
fs.writeFileSync(path.join(seed, 'README.md'), '# hi\n');
|
|
298
|
+
(0, child_process_1.execFileSync)('git', ['-C', seed, 'add', '.']);
|
|
299
|
+
(0, child_process_1.execFileSync)('git', ['-C', seed, '-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-m', 'init']);
|
|
300
|
+
(0, child_process_1.execFileSync)('git', ['-C', seed, 'push', 'origin', 'HEAD:refs/heads/main']);
|
|
301
|
+
const deps = {
|
|
302
|
+
reposBaseDir: repos,
|
|
303
|
+
exists: (p) => fs.existsSync(p),
|
|
304
|
+
mkdir: (p) => fs.mkdirSync(p, { recursive: true }),
|
|
305
|
+
run: async (cmd, args) => {
|
|
306
|
+
(0, child_process_1.execFileSync)(cmd, args);
|
|
307
|
+
return { stdout: '', code: 0 };
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
const first = await (0, repoCheckout_1.ensureWorkspaceRepo)({ workspaceId: 'ws-int', repoUrl: origin }, deps);
|
|
311
|
+
expect(first.action).toBe('cloned');
|
|
312
|
+
expect(fs.existsSync(path.join(repos, 'ws-int', '.git'))).toBe(true);
|
|
313
|
+
const second = await (0, repoCheckout_1.ensureWorkspaceRepo)({ workspaceId: 'ws-int', repoUrl: origin }, deps);
|
|
314
|
+
expect(second.action).toBe('existing');
|
|
315
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
316
|
+
});
|
|
317
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
/**
|
|
7
|
+
* TDD — lws-03: `wen-market devices` list/revoke. Uses this machine's own
|
|
8
|
+
* Connect Token (bridge-config.json) — no interactive login required.
|
|
9
|
+
*/
|
|
10
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
+
const mockLoadBridgeConfig = jest.fn();
|
|
12
|
+
jest.mock('../bridge/config', () => ({
|
|
13
|
+
loadBridgeConfig: () => mockLoadBridgeConfig(),
|
|
14
|
+
}));
|
|
15
|
+
const mockListDevices = jest.fn();
|
|
16
|
+
const mockRevokeDevice = jest.fn();
|
|
17
|
+
jest.mock('../bridge/apiClient', () => ({
|
|
18
|
+
listDevices: (...args) => mockListDevices(...args),
|
|
19
|
+
revokeDevice: (...args) => mockRevokeDevice(...args),
|
|
20
|
+
}));
|
|
21
|
+
jest.mock('../commands/bridge', () => ({ bridgeApiUrl: () => 'https://api.x' }));
|
|
22
|
+
const devices_1 = require("../commands/devices");
|
|
23
|
+
async function runCommand(...args) {
|
|
24
|
+
await devices_1.devicesCommand.parseAsync(args, { from: 'user' });
|
|
25
|
+
}
|
|
26
|
+
beforeAll(() => {
|
|
27
|
+
chalk_1.default.level = 0;
|
|
28
|
+
});
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
jest.clearAllMocks();
|
|
31
|
+
mockLoadBridgeConfig.mockReturnValue({ connectToken: 'wm_ct_' + 'a'.repeat(64), deviceId: 'd-this' });
|
|
32
|
+
});
|
|
33
|
+
describe('devices list', () => {
|
|
34
|
+
it('lists devices and marks the current one', async () => {
|
|
35
|
+
mockListDevices.mockResolvedValue({ success: true, devices: [
|
|
36
|
+
{ id: 'd-this', name: 'bunny-mbp', revokedAt: null },
|
|
37
|
+
{ id: 'd-other', name: 'other-box', revokedAt: null },
|
|
38
|
+
] });
|
|
39
|
+
const spy = jest.spyOn(console, 'log').mockImplementation();
|
|
40
|
+
await runCommand();
|
|
41
|
+
const output = spy.mock.calls.map((c) => c[0]).join('\n');
|
|
42
|
+
expect(output).toContain('bunny-mbp');
|
|
43
|
+
expect(output).toContain('other-box');
|
|
44
|
+
expect(output).toContain('this machine');
|
|
45
|
+
spy.mockRestore();
|
|
46
|
+
});
|
|
47
|
+
it('errors clearly with no Connect Token configured', async () => {
|
|
48
|
+
mockLoadBridgeConfig.mockReturnValue(null);
|
|
49
|
+
const errSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
50
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
51
|
+
await runCommand();
|
|
52
|
+
expect(mockListDevices).not.toHaveBeenCalled();
|
|
53
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
54
|
+
errSpy.mockRestore();
|
|
55
|
+
exitSpy.mockRestore();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('devices revoke', () => {
|
|
59
|
+
it('revokes a device by id', async () => {
|
|
60
|
+
mockRevokeDevice.mockResolvedValue({ success: true });
|
|
61
|
+
const spy = jest.spyOn(console, 'log').mockImplementation();
|
|
62
|
+
await runCommand('revoke', 'd-other');
|
|
63
|
+
expect(mockRevokeDevice).toHaveBeenCalledWith(expect.anything(), 'd-other');
|
|
64
|
+
spy.mockRestore();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
/**
|
|
7
|
+
* TDD — lws-03: `wen-market local add/list/remove`. Folder→workspace
|
|
8
|
+
* registry commands. Mocks the bridge config/apiClient/folderRegistry
|
|
9
|
+
* modules — no real network, no real fs (prior art: workspace.test.ts).
|
|
10
|
+
*/
|
|
11
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
12
|
+
const mockLoadBridgeConfig = jest.fn();
|
|
13
|
+
jest.mock('../bridge/config', () => ({
|
|
14
|
+
loadBridgeConfig: () => mockLoadBridgeConfig(),
|
|
15
|
+
}));
|
|
16
|
+
const mockCreateLocalWorkspace = jest.fn();
|
|
17
|
+
const mockDeleteWorkspace = jest.fn();
|
|
18
|
+
jest.mock('../bridge/apiClient', () => ({
|
|
19
|
+
createLocalWorkspace: (...args) => mockCreateLocalWorkspace(...args),
|
|
20
|
+
deleteWorkspace: (...args) => mockDeleteWorkspace(...args),
|
|
21
|
+
BridgeApiError: class BridgeApiError extends Error {
|
|
22
|
+
code;
|
|
23
|
+
status;
|
|
24
|
+
constructor(message, code, status) { super(message); this.code = code; this.status = status; }
|
|
25
|
+
},
|
|
26
|
+
}));
|
|
27
|
+
jest.mock('../commands/bridge', () => ({ bridgeApiUrl: () => 'https://api.x' }));
|
|
28
|
+
let registryState = { v: 1, entries: {} };
|
|
29
|
+
jest.mock('../bridge/folderRegistry', () => {
|
|
30
|
+
const actual = jest.requireActual('../bridge/folderRegistry');
|
|
31
|
+
return {
|
|
32
|
+
...actual,
|
|
33
|
+
loadFolderRegistry: () => registryState,
|
|
34
|
+
saveFolderRegistry: (registry) => { registryState = registry; },
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
const local_1 = require("../commands/local");
|
|
38
|
+
async function runCommand(...args) {
|
|
39
|
+
await local_1.localCommand.parseAsync(args, { from: 'user' });
|
|
40
|
+
}
|
|
41
|
+
beforeAll(() => {
|
|
42
|
+
chalk_1.default.level = 0;
|
|
43
|
+
});
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
jest.clearAllMocks();
|
|
46
|
+
registryState = { v: 1, entries: {} };
|
|
47
|
+
mockLoadBridgeConfig.mockReturnValue({ connectToken: 'wm_ct_' + 'a'.repeat(64), deviceId: 'device-1' });
|
|
48
|
+
});
|
|
49
|
+
describe('local add', () => {
|
|
50
|
+
it('creates a local workspace and registers the resolved path', async () => {
|
|
51
|
+
mockCreateLocalWorkspace.mockResolvedValue({ success: true, workspace: { id: 'ws-1', name: 'wenrwa-arena' } });
|
|
52
|
+
const spy = jest.spyOn(console, 'log').mockImplementation();
|
|
53
|
+
await runCommand('add', process.cwd());
|
|
54
|
+
expect(mockCreateLocalWorkspace).toHaveBeenCalledWith(expect.objectContaining({ apiUrl: 'https://api.x', connectToken: expect.stringMatching(/^wm_ct_/) }), { name: expect.any(String) });
|
|
55
|
+
expect(registryState.entries[process.cwd()]).toMatchObject({ workspaceId: 'ws-1' });
|
|
56
|
+
const output = spy.mock.calls.map((c) => c[0]).join('\n');
|
|
57
|
+
expect(output).toContain('ws-1');
|
|
58
|
+
spy.mockRestore();
|
|
59
|
+
});
|
|
60
|
+
it('is idempotent — re-adding the same path returns the existing workspace without creating a new one', async () => {
|
|
61
|
+
registryState = { v: 1, entries: { [process.cwd()]: { workspaceId: 'ws-existing', name: 'x', addedAt: 'now' } } };
|
|
62
|
+
const spy = jest.spyOn(console, 'log').mockImplementation();
|
|
63
|
+
await runCommand('add', process.cwd());
|
|
64
|
+
expect(mockCreateLocalWorkspace).not.toHaveBeenCalled();
|
|
65
|
+
const output = spy.mock.calls.map((c) => c[0]).join('\n');
|
|
66
|
+
expect(output).toContain('ws-existing');
|
|
67
|
+
spy.mockRestore();
|
|
68
|
+
});
|
|
69
|
+
it('errors clearly when this machine has not been connected yet (no deviceId)', async () => {
|
|
70
|
+
mockLoadBridgeConfig.mockReturnValue({ connectToken: 'wm_ct_' + 'a'.repeat(64) }); // no deviceId
|
|
71
|
+
const errSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
72
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
73
|
+
await runCommand('add', process.cwd());
|
|
74
|
+
expect(mockCreateLocalWorkspace).not.toHaveBeenCalled();
|
|
75
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
76
|
+
errSpy.mockRestore();
|
|
77
|
+
exitSpy.mockRestore();
|
|
78
|
+
});
|
|
79
|
+
it('surfaces a clear, non-crashing error when the device was revoked (401 mid-registration)', async () => {
|
|
80
|
+
const { BridgeApiError } = jest.requireMock('../bridge/apiClient');
|
|
81
|
+
mockCreateLocalWorkspace.mockRejectedValue(new BridgeApiError('Connect token is invalid or revoked.', 'INVALID_CONNECT_TOKEN', 401));
|
|
82
|
+
const errSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
83
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
84
|
+
await runCommand('add', process.cwd());
|
|
85
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
86
|
+
const output = errSpy.mock.calls.map((c) => c[0]).join('\n');
|
|
87
|
+
expect(output.toLowerCase()).toContain('revoked');
|
|
88
|
+
errSpy.mockRestore();
|
|
89
|
+
exitSpy.mockRestore();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('local list', () => {
|
|
93
|
+
it('prints all registered folders', async () => {
|
|
94
|
+
registryState = {
|
|
95
|
+
v: 1,
|
|
96
|
+
entries: {
|
|
97
|
+
'/Users/x/app-a': { workspaceId: 'ws-a', name: 'app-a', addedAt: 'now' },
|
|
98
|
+
'/Users/x/app-b': { workspaceId: 'ws-b', name: 'app-b', addedAt: 'now' },
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
const spy = jest.spyOn(console, 'log').mockImplementation();
|
|
102
|
+
await runCommand('list');
|
|
103
|
+
const output = spy.mock.calls.map((c) => c[0]).join('\n');
|
|
104
|
+
expect(output).toContain('ws-a');
|
|
105
|
+
expect(output).toContain('ws-b');
|
|
106
|
+
spy.mockRestore();
|
|
107
|
+
});
|
|
108
|
+
it('says so when nothing is registered', async () => {
|
|
109
|
+
const spy = jest.spyOn(console, 'log').mockImplementation();
|
|
110
|
+
await runCommand('list');
|
|
111
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining('No local workspaces registered'));
|
|
112
|
+
spy.mockRestore();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
describe('local remove', () => {
|
|
116
|
+
it('removes the registration only — never deletes the workspace by default', async () => {
|
|
117
|
+
registryState = { v: 1, entries: { '/Users/x/app-a': { workspaceId: 'ws-a', name: 'app-a', addedAt: 'now' } } };
|
|
118
|
+
const spy = jest.spyOn(console, 'log').mockImplementation();
|
|
119
|
+
await runCommand('remove', '/Users/x/app-a');
|
|
120
|
+
expect(registryState.entries['/Users/x/app-a']).toBeUndefined();
|
|
121
|
+
expect(mockDeleteWorkspace).not.toHaveBeenCalled();
|
|
122
|
+
const output = spy.mock.calls.map((c) => c[0]).join('\n');
|
|
123
|
+
expect(output.toLowerCase()).toContain('files untouched');
|
|
124
|
+
spy.mockRestore();
|
|
125
|
+
});
|
|
126
|
+
it('finds the entry by workspaceId too, not just by path', async () => {
|
|
127
|
+
registryState = { v: 1, entries: { '/Users/x/app-a': { workspaceId: 'ws-a', name: 'app-a', addedAt: 'now' } } };
|
|
128
|
+
const spy = jest.spyOn(console, 'log').mockImplementation();
|
|
129
|
+
await runCommand('remove', 'ws-a');
|
|
130
|
+
expect(registryState.entries['/Users/x/app-a']).toBeUndefined();
|
|
131
|
+
spy.mockRestore();
|
|
132
|
+
});
|
|
133
|
+
it('--delete also deletes the workspace row server-side', async () => {
|
|
134
|
+
registryState = { v: 1, entries: { '/Users/x/app-a': { workspaceId: 'ws-a', name: 'app-a', addedAt: 'now' } } };
|
|
135
|
+
mockDeleteWorkspace.mockResolvedValue({ success: true });
|
|
136
|
+
const spy = jest.spyOn(console, 'log').mockImplementation();
|
|
137
|
+
await runCommand('remove', '/Users/x/app-a', '--delete');
|
|
138
|
+
expect(mockDeleteWorkspace).toHaveBeenCalledWith(expect.anything(), 'ws-a');
|
|
139
|
+
spy.mockRestore();
|
|
140
|
+
});
|
|
141
|
+
it('errors clearly when the path/id is not registered', async () => {
|
|
142
|
+
const errSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
143
|
+
await runCommand('remove', '/nope');
|
|
144
|
+
expect(errSpy).toHaveBeenCalledWith(expect.stringContaining('not registered'));
|
|
145
|
+
errSpy.mockRestore();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pucc-int-03 (ADR-0032) — Claude Code PTY activity parser (the PRODUCER).
|
|
3
|
+
*
|
|
4
|
+
* A Claude Code agent is a real PTY session the platform never drives
|
|
5
|
+
* (ADR-0028). To give it the same file-targeted drones/holograms a credits
|
|
6
|
+
* agent gets, the bridge OBSERVES its PTY output (read-only) and extracts the
|
|
7
|
+
* tool-use it renders — Edit/Write/Read/Bash targets + a short "thinking"
|
|
8
|
+
* summary — so the arena can swarm the files it touches.
|
|
9
|
+
*
|
|
10
|
+
* This module is PURE and defensive by contract:
|
|
11
|
+
* - Input is raw PTY output: ANSI escapes, \r cursor redraws, and tool lines
|
|
12
|
+
* split arbitrarily across chunks. It buffers partial lines across `push`.
|
|
13
|
+
* - A parse MISS yields NOTHING (empty array). It NEVER throws — every code
|
|
14
|
+
* path that could fail is caught and degraded to "no signal", because a
|
|
15
|
+
* parser bug must never break the terminal it is only watching.
|
|
16
|
+
* - The recognized markers are a SMALL, VERSIONED set (see TOOL_VERBS): if
|
|
17
|
+
* Claude Code's output format drifts, this table is the one thing to bump.
|
|
18
|
+
*
|
|
19
|
+
* Recognized shape (best-effort): Claude Code renders a tool call as a bulleted
|
|
20
|
+
* line — `● Edit(src/foo.ts)`, `⏺ Read(package.json)`, `● Bash(npm test)`,
|
|
21
|
+
* `⏺ Update(src/x.ts)` — a bullet glyph, the Tool name, and a parenthesized
|
|
22
|
+
* arg. For file tools the arg's first token is the (repo-relative) path; for
|
|
23
|
+
* Bash it is a command (no path is extracted — the consumer degrades an empty
|
|
24
|
+
* `filePaths` to an ambient pulse). Every recognized tool also yields a
|
|
25
|
+
* human "thinking" phrase ("Editing foo.ts", "Running npm test").
|
|
26
|
+
*/
|
|
27
|
+
export interface ActivitySignal {
|
|
28
|
+
/** Repo-relative file targets extracted from a file-tool call (may be empty). */
|
|
29
|
+
filePaths: string[];
|
|
30
|
+
/** Short human summary of what the agent is doing, e.g. "Editing foo.ts". */
|
|
31
|
+
thinking?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface ActivityParserOpts {
|
|
34
|
+
/** A single un-terminated line longer than this is abandoned (binary spew /
|
|
35
|
+
* a pathological redraw) — the buffer is trimmed to its tail so a later
|
|
36
|
+
* newline can still complete a plausible line. Default 16384. */
|
|
37
|
+
maxLineLength?: number;
|
|
38
|
+
}
|
|
39
|
+
export declare class ActivityParser {
|
|
40
|
+
private buf;
|
|
41
|
+
private maxLineLength;
|
|
42
|
+
/** Suppress consecutive identical signals (TUI re-renders the same line). */
|
|
43
|
+
private lastKey;
|
|
44
|
+
constructor(opts?: ActivityParserOpts);
|
|
45
|
+
/**
|
|
46
|
+
* Feed a chunk of PTY output. Returns every NEW tool-use signal completed by
|
|
47
|
+
* this chunk (usually 0 or 1). Never throws: any internal failure degrades
|
|
48
|
+
* to an empty result so the caller's PTY forwarding is never disturbed.
|
|
49
|
+
*/
|
|
50
|
+
push(chunk: string): ActivitySignal[];
|
|
51
|
+
private pushUnsafe;
|
|
52
|
+
private parseLine;
|
|
53
|
+
}
|