@wenrwa/marketplace-cli 0.6.5 → 0.7.0
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/README.md +85 -85
- package/dist/__tests__/auth-login-core.test.js +2 -2
- package/dist/__tests__/auth.test.js +4 -4
- package/dist/__tests__/bridge-autostart.test.js +9 -9
- package/dist/__tests__/bridge-checkout-dirty-real-git.test.d.ts +1 -0
- package/dist/__tests__/bridge-checkout-dirty-real-git.test.js +145 -0
- package/dist/__tests__/bridge-checkout-inventory-reporter.test.js +6 -2
- package/dist/__tests__/bridge-checkout-inventory.test.js +128 -14
- package/dist/__tests__/bridge-checkout-park-and-remove.test.js +57 -8
- package/dist/__tests__/bridge-claude-trust.test.d.ts +1 -0
- package/dist/__tests__/bridge-claude-trust.test.js +137 -0
- package/dist/__tests__/bridge-command-browser-options.test.js +1 -1
- package/dist/__tests__/bridge-command-install-env.test.js +1 -1
- package/dist/__tests__/bridge-device-registration.test.js +1 -1
- package/dist/__tests__/bridge-folder-registry.test.js +1 -1
- package/dist/__tests__/bridge-local-add-core.test.js +1 -1
- package/dist/__tests__/bridge-match-pointer.test.d.ts +1 -0
- package/dist/__tests__/bridge-match-pointer.test.js +136 -0
- package/dist/__tests__/bridge-pairing-api.test.js +1 -1
- package/dist/__tests__/bridge-pairing-core.test.js +1 -1
- package/dist/__tests__/bridge-park-remove-frame.test.js +61 -0
- package/dist/__tests__/bridge-relay-client-checkout-inventory.test.js +1 -1
- package/dist/__tests__/bridge-relay-mode.test.js +1 -1
- package/dist/__tests__/bridge-rival-touch-forwarder.test.d.ts +1 -0
- package/dist/__tests__/bridge-rival-touch-forwarder.test.js +266 -0
- package/dist/__tests__/bridge-server.test.js +1 -1
- package/dist/__tests__/config.test.js +1 -1
- package/dist/__tests__/devices.test.js +1 -1
- package/dist/__tests__/local.test.js +1 -1
- package/dist/__tests__/openBrowser.test.js +1 -1
- package/dist/__tests__/websocket.test.js +3 -3
- package/dist/auth/identity.d.ts +1 -1
- package/dist/auth/login.d.ts +2 -2
- package/dist/bridge/apiClient.d.ts +14 -0
- package/dist/bridge/apiClient.js +19 -2
- package/dist/bridge/autostart.d.ts +1 -1
- package/dist/bridge/autostart.js +1 -1
- package/dist/bridge/checkoutInventory.d.ts +48 -17
- package/dist/bridge/checkoutInventory.js +190 -44
- package/dist/bridge/checkoutParkAndRemove.d.ts +22 -1
- package/dist/bridge/checkoutParkAndRemove.js +37 -7
- package/dist/bridge/claudeTrust.d.ts +37 -0
- package/dist/bridge/claudeTrust.js +85 -0
- package/dist/bridge/config.d.ts +1 -1
- package/dist/bridge/contextWriter.d.ts +2 -0
- package/dist/bridge/contextWriter.js +3 -2
- package/dist/bridge/deviceRegistration.d.ts +1 -1
- package/dist/bridge/envSanitize.d.ts +1 -1
- package/dist/bridge/envSanitize.js +1 -1
- package/dist/bridge/folderRegistry.js +1 -1
- package/dist/bridge/localAdd.js +1 -1
- package/dist/bridge/localContextWriter.d.ts +2 -0
- package/dist/bridge/localContextWriter.js +3 -2
- package/dist/bridge/memoryBrief.d.ts +16 -4
- package/dist/bridge/memoryBrief.js +17 -9
- package/dist/bridge/pairing.d.ts +1 -1
- package/dist/bridge/relayClient.js +11 -1
- package/dist/bridge/rivalTouchForwarder.d.ts +48 -0
- package/dist/bridge/rivalTouchForwarder.js +211 -0
- package/dist/bridge/server.d.ts +14 -1
- package/dist/bridge/server.js +24 -0
- package/dist/bridge/workGraphPointer.d.ts +19 -0
- package/dist/bridge/workGraphPointer.js +26 -1
- package/dist/commands/apps.js +1 -1
- package/dist/commands/auth.d.ts +1 -1
- package/dist/commands/auth.js +6 -6
- package/dist/commands/bridge.js +50 -13
- package/dist/commands/credits.js +1 -1
- package/dist/commands/devices.d.ts +1 -1
- package/dist/commands/devices.js +4 -4
- package/dist/commands/local.d.ts +1 -1
- package/dist/commands/local.js +5 -5
- package/dist/commands/pair.d.ts +1 -1
- package/dist/commands/pair.js +4 -4
- package/dist/index.js +2 -2
- package/dist/utils/config.js +2 -2
- package/dist/utils/openBrowser.d.ts +1 -1
- package/dist/utils/openBrowser.js +1 -1
- package/dist/utils/websocket.js +1 -1
- package/package.json +3 -2
|
@@ -349,4 +349,65 @@ describe('relay client — park-remove frame (hcs-06)', () => {
|
|
|
349
349
|
await closeRelay(relay.wss);
|
|
350
350
|
}
|
|
351
351
|
});
|
|
352
|
+
it('park-save backs up and KEEPS the checkout: committing, pushing, then a saved result — no remove', async () => {
|
|
353
|
+
const relay = await startFakeRelay();
|
|
354
|
+
const sessions = new sessionManager_1.SessionManager(makeFactory());
|
|
355
|
+
const gitFixture = makeGitFixture();
|
|
356
|
+
const reclaimFs = makeReclaimFsFixture([`${REPOS}/ws-1`]);
|
|
357
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
358
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
359
|
+
relayToken: 'tok',
|
|
360
|
+
sessions,
|
|
361
|
+
enableReclaim: true,
|
|
362
|
+
reposBaseDir: REPOS,
|
|
363
|
+
checkoutReclaimDeps: reclaimFs,
|
|
364
|
+
enableParkAndRemove: true,
|
|
365
|
+
checkoutParkAndRemoveDeps: { exists: reclaimFs.exists, run: gitFixture.run },
|
|
366
|
+
});
|
|
367
|
+
try {
|
|
368
|
+
const ws = await relay.nextConnection();
|
|
369
|
+
const q = frameQueue(ws);
|
|
370
|
+
await q.next(); // auth
|
|
371
|
+
ws.send(JSON.stringify({ t: 'park-save', workspaceId: 'ws-1' }));
|
|
372
|
+
expect(await q.next()).toMatchObject({ t: 'park-remove-progress', phase: 'committing' });
|
|
373
|
+
expect(await q.next()).toMatchObject({ t: 'park-remove-progress', phase: 'pushing' });
|
|
374
|
+
expect(await q.next()).toMatchObject({ t: 'park-remove-result', workspaceId: 'ws-1', outcome: 'saved', branch: 'park/ws-1' });
|
|
375
|
+
expect(reclaimFs.removed).toEqual([]);
|
|
376
|
+
}
|
|
377
|
+
finally {
|
|
378
|
+
handle.close();
|
|
379
|
+
await closeRelay(relay.wss);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
it('inventory-scan re-scans and sends a fresh checkout-inventory frame', async () => {
|
|
383
|
+
const relay = await startFakeRelay();
|
|
384
|
+
const sessions = new sessionManager_1.SessionManager(makeFactory());
|
|
385
|
+
let lists = 0;
|
|
386
|
+
const handle = (0, relayClient_1.startRelayClient)({
|
|
387
|
+
relayUrl: `ws://127.0.0.1:${relay.port}`,
|
|
388
|
+
relayToken: 'tok',
|
|
389
|
+
sessions,
|
|
390
|
+
reportCheckoutInventory: true,
|
|
391
|
+
reposBaseDir: REPOS,
|
|
392
|
+
checkoutInventoryDeps: {
|
|
393
|
+
listCheckouts: () => { lists += 1; return []; },
|
|
394
|
+
statMtimeMs: () => 1,
|
|
395
|
+
sizeBytes: async () => 0,
|
|
396
|
+
run: async () => ({ stdout: '', code: 0 }),
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
try {
|
|
400
|
+
const ws = await relay.nextConnection();
|
|
401
|
+
const q = frameQueue(ws);
|
|
402
|
+
await q.next(); // auth
|
|
403
|
+
expect(await q.next()).toMatchObject({ t: 'checkout-inventory' }); // on-connect scan
|
|
404
|
+
ws.send(JSON.stringify({ t: 'inventory-scan' }));
|
|
405
|
+
expect(await q.next()).toEqual({ t: 'checkout-inventory', checkouts: [] });
|
|
406
|
+
expect(lists).toBe(2);
|
|
407
|
+
}
|
|
408
|
+
finally {
|
|
409
|
+
handle.close();
|
|
410
|
+
await closeRelay(relay.wss);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
352
413
|
});
|
|
@@ -127,7 +127,7 @@ describe('relay client — checkout inventory scan never blocks connect (hcs-02
|
|
|
127
127
|
const inventory = await q.next();
|
|
128
128
|
expect(inventory).toEqual({
|
|
129
129
|
t: 'checkout-inventory',
|
|
130
|
-
checkouts: [{ workspaceId: 'ws-a', unparseable: false, bytes: 4096, dirty: false, mtimeMs: 100 }],
|
|
130
|
+
checkouts: [{ workspaceId: 'ws-a', unparseable: false, bytes: 4096, dirty: false, mtimeMs: 100, dirtyKinds: [], noUpstream: false, scanVersion: 2 }],
|
|
131
131
|
});
|
|
132
132
|
handle.close();
|
|
133
133
|
await closeRelay(relay.wss);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const bridge_1 = require("../commands/bridge");
|
|
4
|
-
describe('resolveRelayMode — CLI/env wiring for `
|
|
4
|
+
describe('resolveRelayMode — CLI/env wiring for `hiveop bridge --relay`', () => {
|
|
5
5
|
it('no --relay flag and no RELAY_URL env → local mode', () => {
|
|
6
6
|
expect((0, bridge_1.resolveRelayMode)({}, {})).toEqual({ mode: 'local' });
|
|
7
7
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,266 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
/**
|
|
40
|
+
* rival-10 — a LOCAL agent's live file touches reach a ranked rival, like a
|
|
41
|
+
* hosted agent's. What leaves the laptop: agent id + file paths, only for a
|
|
42
|
+
* workspace the server says is a live ranked match this user owns. Nothing
|
|
43
|
+
* else, and nothing at all for any other workspace.
|
|
44
|
+
*/
|
|
45
|
+
const ws_1 = __importDefault(require("ws"));
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const os = __importStar(require("os"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const server_1 = require("../bridge/server");
|
|
50
|
+
const sessionManager_1 = require("../bridge/sessionManager");
|
|
51
|
+
const rivalTouchForwarder_1 = require("../bridge/rivalTouchForwarder");
|
|
52
|
+
const apiClient_1 = require("../bridge/apiClient");
|
|
53
|
+
const MATCH_WS = 'bbbbbbbb-0000-4000-8000-00000000000b';
|
|
54
|
+
const OTHER_WS = 'dddddddd-0000-4000-8000-00000000000d';
|
|
55
|
+
const CHECKOUT = '/Users/bob/wenrwa-workspaces/m-11111111-b';
|
|
56
|
+
function harness(statusOf, opts = {}) {
|
|
57
|
+
let now = 1_000_000;
|
|
58
|
+
const asked = [];
|
|
59
|
+
const sent = [];
|
|
60
|
+
const fwd = (0, rivalTouchForwarder_1.createRivalTouchForwarder)({
|
|
61
|
+
status: async (ws) => {
|
|
62
|
+
asked.push(ws);
|
|
63
|
+
const s = statusOf[ws] ?? { live: false, recheckMs: 600_000 };
|
|
64
|
+
if (s instanceof Error)
|
|
65
|
+
throw s;
|
|
66
|
+
return s;
|
|
67
|
+
},
|
|
68
|
+
send: async (workspaceId, touches) => {
|
|
69
|
+
if (opts.sendFails)
|
|
70
|
+
throw new Error('409 RANKED_RIVAL_CLOSED');
|
|
71
|
+
sent.push({ workspaceId, touches });
|
|
72
|
+
},
|
|
73
|
+
now: () => now,
|
|
74
|
+
scheduleInterval: () => 'h',
|
|
75
|
+
cancelInterval: () => undefined,
|
|
76
|
+
});
|
|
77
|
+
return { fwd, asked, sent, advance: (ms) => void (now += ms) };
|
|
78
|
+
}
|
|
79
|
+
describe('forwardablePaths — only paths, and only inside the checkout', () => {
|
|
80
|
+
it('relativises an absolute path under the checkout; keeps relative paths', () => {
|
|
81
|
+
expect((0, rivalTouchForwarder_1.forwardablePaths)([`${CHECKOUT}/src/planner.ts`, 'src/b.ts'], CHECKOUT)).toEqual(['src/planner.ts', 'src/b.ts']);
|
|
82
|
+
});
|
|
83
|
+
it('drops a path outside the checkout, climbing out, non-strings, duplicates', () => {
|
|
84
|
+
expect((0, rivalTouchForwarder_1.forwardablePaths)(['/etc/hosts', `${CHECKOUT}/../x.ts`, '../up.ts', 42, null, 'a.ts', 'a.ts'], CHECKOUT)).toEqual(['a.ts']);
|
|
85
|
+
expect((0, rivalTouchForwarder_1.forwardablePaths)('src/a.ts', CHECKOUT)).toEqual([]);
|
|
86
|
+
});
|
|
87
|
+
it('no known checkout: an absolute path is kept for the server to place (or drop)', () => {
|
|
88
|
+
expect((0, rivalTouchForwarder_1.forwardablePaths)(['/home/x/m-b/src/a.ts'])).toEqual(['/home/x/m-b/src/a.ts']);
|
|
89
|
+
});
|
|
90
|
+
it('caps paths per touch', () => {
|
|
91
|
+
expect((0, rivalTouchForwarder_1.forwardablePaths)(Array.from({ length: 40 }, (_, i) => `f${i}.ts`))).toHaveLength(20);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe('createRivalTouchForwarder', () => {
|
|
95
|
+
it('a live match workspace: the touch goes up as agent id + paths — nothing else', async () => {
|
|
96
|
+
const h = harness({ [MATCH_WS]: { live: true, recheckMs: 15_000 } });
|
|
97
|
+
h.fwd.touch({
|
|
98
|
+
workspaceId: MATCH_WS,
|
|
99
|
+
agentId: 'agent-1',
|
|
100
|
+
filePaths: [`${CHECKOUT}/src/planner.ts`],
|
|
101
|
+
cwd: CHECKOUT,
|
|
102
|
+
// A caller passing more than it should: never forwarded.
|
|
103
|
+
...{ thinking: 'Editing planner.ts', tool: 'Edit', content: 'secret code' },
|
|
104
|
+
});
|
|
105
|
+
await h.fwd.flush();
|
|
106
|
+
expect(h.asked).toEqual([MATCH_WS]);
|
|
107
|
+
expect(h.sent).toEqual([{ workspaceId: MATCH_WS, touches: [{ agentId: 'agent-1', filePaths: ['src/planner.ts'] }] }]);
|
|
108
|
+
const json = JSON.stringify(h.sent);
|
|
109
|
+
for (const w of ['Editing', 'Edit"', 'secret code', '/Users/bob'])
|
|
110
|
+
expect(json).not.toContain(w);
|
|
111
|
+
});
|
|
112
|
+
it('any other workspace: asked once (workspace id only), then nothing sent — and not asked again until recheck', async () => {
|
|
113
|
+
const h = harness({ [OTHER_WS]: { live: false, recheckMs: 600_000 } });
|
|
114
|
+
h.fwd.touch({ workspaceId: OTHER_WS, agentId: 'agent-1', filePaths: ['src/a.ts'] });
|
|
115
|
+
await h.fwd.flush();
|
|
116
|
+
h.fwd.touch({ workspaceId: OTHER_WS, agentId: 'agent-1', filePaths: ['src/b.ts'] });
|
|
117
|
+
await h.fwd.flush();
|
|
118
|
+
expect(h.sent).toEqual([]);
|
|
119
|
+
expect(h.asked).toEqual([OTHER_WS]);
|
|
120
|
+
});
|
|
121
|
+
it('before GO the server says "ask again soon"; touches then are dropped, the ones after GO fly', async () => {
|
|
122
|
+
const status = { [MATCH_WS]: { live: false, recheckMs: 5_000 } };
|
|
123
|
+
const h = harness(status);
|
|
124
|
+
h.fwd.touch({ workspaceId: MATCH_WS, agentId: 'a', filePaths: ['early.ts'] });
|
|
125
|
+
await h.fwd.flush();
|
|
126
|
+
expect(h.sent).toEqual([]);
|
|
127
|
+
status[MATCH_WS] = { live: true, recheckMs: 15_000 };
|
|
128
|
+
h.advance(5_001);
|
|
129
|
+
h.fwd.touch({ workspaceId: MATCH_WS, agentId: 'a', filePaths: ['late.ts'] });
|
|
130
|
+
await h.fwd.flush();
|
|
131
|
+
expect(h.sent.flatMap((s) => s.touches)).toEqual([{ agentId: 'a', filePaths: ['late.ts'] }]);
|
|
132
|
+
});
|
|
133
|
+
it('the match ends: the next status says no, and forwarding stops', async () => {
|
|
134
|
+
const status = { [MATCH_WS]: { live: true, recheckMs: 15_000 } };
|
|
135
|
+
const h = harness(status);
|
|
136
|
+
h.fwd.touch({ workspaceId: MATCH_WS, agentId: 'a', filePaths: ['one.ts'] });
|
|
137
|
+
await h.fwd.flush();
|
|
138
|
+
status[MATCH_WS] = { live: false, recheckMs: 600_000 };
|
|
139
|
+
h.advance(15_001);
|
|
140
|
+
h.fwd.touch({ workspaceId: MATCH_WS, agentId: 'a', filePaths: ['two.ts'] });
|
|
141
|
+
await h.fwd.flush();
|
|
142
|
+
expect(h.sent.flatMap((s) => s.touches.flatMap((t) => t.filePaths))).toEqual(['one.ts']);
|
|
143
|
+
});
|
|
144
|
+
it('a status failure is "not live": nothing is sent', async () => {
|
|
145
|
+
const h = harness({ [MATCH_WS]: new Error('offline') });
|
|
146
|
+
h.fwd.touch({ workspaceId: MATCH_WS, agentId: 'a', filePaths: ['x.ts'] });
|
|
147
|
+
await h.fwd.flush();
|
|
148
|
+
expect(h.sent).toEqual([]);
|
|
149
|
+
});
|
|
150
|
+
it('a refused POST stops forwarding for that workspace until it is asked again', async () => {
|
|
151
|
+
const h = harness({ [MATCH_WS]: { live: true, recheckMs: 15_000 } }, { sendFails: true });
|
|
152
|
+
h.fwd.touch({ workspaceId: MATCH_WS, agentId: 'a', filePaths: ['x.ts'] });
|
|
153
|
+
await h.fwd.flush();
|
|
154
|
+
h.fwd.touch({ workspaceId: MATCH_WS, agentId: 'a', filePaths: ['y.ts'] });
|
|
155
|
+
await h.fwd.flush();
|
|
156
|
+
expect(h.asked).toEqual([MATCH_WS]); // backed off: y.ts was dropped, not re-asked at once
|
|
157
|
+
});
|
|
158
|
+
it('a touch with no forwardable path, or no workspace, leaves nothing and asks nothing', async () => {
|
|
159
|
+
const h = harness({ [MATCH_WS]: { live: true, recheckMs: 15_000 } });
|
|
160
|
+
h.fwd.touch({ workspaceId: MATCH_WS, agentId: 'a', filePaths: ['/etc/hosts'], cwd: CHECKOUT });
|
|
161
|
+
h.fwd.touch({ workspaceId: '', agentId: 'a', filePaths: ['x.ts'] });
|
|
162
|
+
h.fwd.touch({ workspaceId: MATCH_WS, agentId: 'a', filePaths: undefined });
|
|
163
|
+
await h.fwd.flush();
|
|
164
|
+
expect(h.asked).toEqual([]);
|
|
165
|
+
expect(h.sent).toEqual([]);
|
|
166
|
+
});
|
|
167
|
+
it('batches: many touches in one flush go in requests of at most 50', async () => {
|
|
168
|
+
const h = harness({ [MATCH_WS]: { live: true, recheckMs: 15_000 } });
|
|
169
|
+
for (let i = 0; i < 120; i++)
|
|
170
|
+
h.fwd.touch({ workspaceId: MATCH_WS, agentId: 'a', filePaths: [`f${i}.ts`] });
|
|
171
|
+
await h.fwd.flush();
|
|
172
|
+
expect(h.sent.map((s) => s.touches.length)).toEqual([50, 50, 20]);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
describe('apiClient — the two calls, over the Connect Token', () => {
|
|
176
|
+
it('status sends the workspace id only; POST sends { touches: [{ agentId, filePaths }] } only', async () => {
|
|
177
|
+
const calls = [];
|
|
178
|
+
const fetchImpl = async (url, init) => {
|
|
179
|
+
calls.push({ url, init });
|
|
180
|
+
return { ok: true, status: 200, json: async () => ({ success: true, data: { live: true, recheckMs: 15_000 } }) };
|
|
181
|
+
};
|
|
182
|
+
const api = { apiUrl: 'https://api.example/api/v1', connectToken: 'wm_ct_x', fetchImpl };
|
|
183
|
+
await expect((0, apiClient_1.getRivalTouchStatus)(api, MATCH_WS)).resolves.toEqual({ live: true, recheckMs: 15_000 });
|
|
184
|
+
await (0, apiClient_1.postRivalTouches)(api, MATCH_WS, [{ agentId: 'a', filePaths: ['src/x.ts'], thinking: 'no' }]);
|
|
185
|
+
expect(calls[0].url).toBe(`https://api.example/api/v1/ranked/workspaces/${MATCH_WS}/touches`);
|
|
186
|
+
expect(calls[0].init.method).toBe('GET');
|
|
187
|
+
expect(calls[0].init.body).toBeUndefined();
|
|
188
|
+
expect(calls[0].init.headers.Authorization).toBe('Bearer wm_ct_x');
|
|
189
|
+
expect(calls[1].init.method).toBe('POST');
|
|
190
|
+
expect(JSON.parse(calls[1].init.body)).toEqual({ touches: [{ agentId: 'a', filePaths: ['src/x.ts'] }] });
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
// ── the loopback server: which records reach the forwarder ──────────────────
|
|
194
|
+
class FakePty {
|
|
195
|
+
write() { }
|
|
196
|
+
resize() { }
|
|
197
|
+
kill() { }
|
|
198
|
+
onData() { }
|
|
199
|
+
onExit() { }
|
|
200
|
+
}
|
|
201
|
+
const ORIGIN = 'https://app.hiveop.io';
|
|
202
|
+
const TOKEN = 'session-token-123';
|
|
203
|
+
describe('bridge server → forwardRivalTouch', () => {
|
|
204
|
+
it("a local workspace agent's tailed record hands on workspace + agent + paths (never thinking/tool); a record for any other session hands on nothing", async () => {
|
|
205
|
+
const file = fs.mkdtempSync(path.join(os.tmpdir(), 'rival10-activity-')) + '/activity.jsonl';
|
|
206
|
+
fs.writeFileSync(file, '');
|
|
207
|
+
const tick = { fn: null };
|
|
208
|
+
const forwarded = [];
|
|
209
|
+
const handle = await (0, server_1.startBridgeServer)({
|
|
210
|
+
sessionToken: TOKEN,
|
|
211
|
+
allowedOrigin: ORIGIN,
|
|
212
|
+
sessions: new sessionManager_1.SessionManager({ spawn: () => new FakePty() }),
|
|
213
|
+
resolveLocalAgentCwd: () => CHECKOUT,
|
|
214
|
+
activityFile: file,
|
|
215
|
+
hiveOpSettingsPath: '/Users/t/.wen-market/claude-hooks-settings.json',
|
|
216
|
+
activityScheduleInterval: (cb) => { tick.fn = cb; return 'h'; },
|
|
217
|
+
activityCancelInterval: () => { },
|
|
218
|
+
forwardRivalTouch: (t) => forwarded.push(t),
|
|
219
|
+
});
|
|
220
|
+
try {
|
|
221
|
+
const ws = new ws_1.default(`ws://127.0.0.1:${handle.port}`, { headers: { origin: ORIGIN } });
|
|
222
|
+
const frames = [];
|
|
223
|
+
ws.on('message', (raw) => frames.push(JSON.parse(String(raw))));
|
|
224
|
+
await new Promise((r) => ws.on('open', r));
|
|
225
|
+
ws.send(JSON.stringify({ v: 1, token: TOKEN }));
|
|
226
|
+
ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24, agentId: 'agent-1', workspaceId: MATCH_WS }));
|
|
227
|
+
await waitFor(() => frames.some((f) => f.t === 'opened'));
|
|
228
|
+
fs.appendFileSync(file, [
|
|
229
|
+
{ sessionId: 'agent-1', filePaths: [`${CHECKOUT}/src/planner.ts`], thinking: 'Editing planner.ts', tool: 'Edit' },
|
|
230
|
+
{ sessionId: 'agent-1', thinking: 'Running npm test', tool: 'Bash' }, // no paths: nothing to forward
|
|
231
|
+
{ sessionId: 'crew-member-9', filePaths: ['src/other.ts'] }, // not a loopback agent of a workspace
|
|
232
|
+
].map((r) => JSON.stringify(r)).join('\n') + '\n');
|
|
233
|
+
tick.fn?.();
|
|
234
|
+
await waitFor(() => frames.filter((f) => f.t === 'activity').length === 3);
|
|
235
|
+
expect(forwarded).toEqual([
|
|
236
|
+
{ workspaceId: MATCH_WS, agentId: 'agent-1', filePaths: [`${CHECKOUT}/src/planner.ts`], cwd: CHECKOUT },
|
|
237
|
+
]);
|
|
238
|
+
// The pilot's own browser still gets the full frame, unchanged.
|
|
239
|
+
expect(frames.find((f) => f.t === 'activity')).toEqual({
|
|
240
|
+
t: 'activity',
|
|
241
|
+
sessionId: 'agent-1',
|
|
242
|
+
filePaths: [`${CHECKOUT}/src/planner.ts`],
|
|
243
|
+
thinking: 'Editing planner.ts',
|
|
244
|
+
});
|
|
245
|
+
// Closed: its later records are not forwarded.
|
|
246
|
+
ws.send(JSON.stringify({ t: 'close', sessionId: 'agent-1' }));
|
|
247
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
248
|
+
fs.appendFileSync(file, JSON.stringify({ sessionId: 'agent-1', filePaths: ['src/late.ts'] }) + '\n');
|
|
249
|
+
tick.fn?.();
|
|
250
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
251
|
+
expect(forwarded).toHaveLength(1);
|
|
252
|
+
ws.close();
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
await handle.close();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
async function waitFor(cond, ms = 2_000) {
|
|
260
|
+
const until = Date.now() + ms;
|
|
261
|
+
while (!cond()) {
|
|
262
|
+
if (Date.now() > until)
|
|
263
|
+
throw new Error('timed out');
|
|
264
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -182,7 +182,7 @@ describe('bridge WebSocket server', () => {
|
|
|
182
182
|
});
|
|
183
183
|
const before = await (await fetch(`http://127.0.0.1:${handle.port}/discovery`, { headers: { origin: ORIGIN } })).json();
|
|
184
184
|
expect(before.localWorkspaces).toEqual([]);
|
|
185
|
-
// Simulate `
|
|
185
|
+
// Simulate `hiveop local add` mutating the registry mid-run.
|
|
186
186
|
entries = [{ workspaceId: 'ws-1', name: 'my-app', path: 'my-app' }];
|
|
187
187
|
const after = await (await fetch(`http://127.0.0.1:${handle.port}/discovery`, { headers: { origin: ORIGIN } })).json();
|
|
188
188
|
expect(after.localWorkspaces).toEqual([{ workspaceId: 'ws-1', name: 'my-app', path: 'my-app' }]);
|
|
@@ -98,7 +98,7 @@ describe('requireConfig()', () => {
|
|
|
98
98
|
(0, config_1.requireConfig)();
|
|
99
99
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
100
100
|
const messages = errSpy.mock.calls.map(c => c.join(' ')).join('\n');
|
|
101
|
-
expect(messages).toContain('
|
|
101
|
+
expect(messages).toContain('hiveop auth login');
|
|
102
102
|
exitSpy.mockRestore();
|
|
103
103
|
errSpy.mockRestore();
|
|
104
104
|
});
|
|
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
/**
|
|
7
|
-
* TDD — lws-03: `
|
|
7
|
+
* TDD — lws-03: `hiveop devices` list/revoke. Uses this machine's own
|
|
8
8
|
* Connect Token (bridge-config.json) — no interactive login required.
|
|
9
9
|
*/
|
|
10
10
|
const chalk_1 = __importDefault(require("chalk"));
|
|
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
/**
|
|
7
|
-
* TDD — lws-03: `
|
|
7
|
+
* TDD — lws-03: `hiveop local add/list/remove`. Folder→workspace
|
|
8
8
|
* registry commands. Mocks the bridge config/apiClient/folderRegistry
|
|
9
9
|
* modules — no real network, no real fs (prior art: workspace.test.ts).
|
|
10
10
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
/**
|
|
4
|
-
* TDD — wha-08: `
|
|
4
|
+
* TDD — wha-08: `hiveop auth login` tries to open the approval URL in a
|
|
5
5
|
* browser as best-effort convenience. The code + URL are always printed
|
|
6
6
|
* regardless (works over SSH), so any failure here must be swallowed, never
|
|
7
7
|
* surfaced. spawnFn is injectable so this never touches a real process.
|
|
@@ -117,7 +117,7 @@ describe('connectWebSocket() handshake (sio-07)', () => {
|
|
|
117
117
|
err.data = { code: 'CREDENTIAL_INVALID' };
|
|
118
118
|
fake.emit('connect_error', err);
|
|
119
119
|
const printed = errorSpy.mock.calls.map((c) => c.join(' ')).join('\n');
|
|
120
|
-
expect(printed).toMatch(/
|
|
120
|
+
expect(printed).toMatch(/hiveop auth login/);
|
|
121
121
|
errorSpy.mockRestore();
|
|
122
122
|
});
|
|
123
123
|
it('shows a clear message and tells the user to log in again on SESSION_REVOKED', () => {
|
|
@@ -129,7 +129,7 @@ describe('connectWebSocket() handshake (sio-07)', () => {
|
|
|
129
129
|
err.data = { code: 'SESSION_REVOKED' };
|
|
130
130
|
fake.emit('connect_error', err);
|
|
131
131
|
const printed = errorSpy.mock.calls.map((c) => c.join(' ')).join('\n');
|
|
132
|
-
expect(printed).toMatch(/
|
|
132
|
+
expect(printed).toMatch(/hiveop auth login/);
|
|
133
133
|
errorSpy.mockRestore();
|
|
134
134
|
});
|
|
135
135
|
it('falls back to a generic message for other connect errors', () => {
|
|
@@ -140,7 +140,7 @@ describe('connectWebSocket() handshake (sio-07)', () => {
|
|
|
140
140
|
fake.emit('connect_error', new Error('ECONNREFUSED'));
|
|
141
141
|
const printed = errorSpy.mock.calls.map((c) => c.join(' ')).join('\n');
|
|
142
142
|
expect(printed).toContain('ECONNREFUSED');
|
|
143
|
-
expect(printed).not.toMatch(/
|
|
143
|
+
expect(printed).not.toMatch(/hiveop auth login/);
|
|
144
144
|
errorSpy.mockRestore();
|
|
145
145
|
});
|
|
146
146
|
it('subscribes to the requested channel on connect, unaffected by the auth change', () => {
|
package/dist/auth/identity.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* wha-08 — the one authenticated call `
|
|
2
|
+
* wha-08 — the one authenticated call `hiveop auth login` (device flow,
|
|
3
3
|
* to print "Signed in as <name>") and `auth login --api-key` (CI, to verify
|
|
4
4
|
* the key before saving it) both need. Sends the key as X-API-Key — never
|
|
5
5
|
* X-Wallet-Pubkey, the retired header this whole PRD exists to remove.
|
package/dist/auth/login.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* wha-08 — `
|
|
2
|
+
* wha-08 — `hiveop auth login`: CLI browser login. Device-code pairing
|
|
3
3
|
* (purpose=cli_login on the shared lws-14 mechanism), same GitHub-CLI /
|
|
4
4
|
* Apple-TV pattern as bridge pairing (bridge/pairing.ts) — a code is minted,
|
|
5
5
|
* printed with the approval URL, and polled until a human decides in the
|
|
6
6
|
* browser.
|
|
7
7
|
*
|
|
8
|
-
* Deliberately NOT shared with bridge/pairing.ts: `
|
|
8
|
+
* Deliberately NOT shared with bridge/pairing.ts: `hiveop pair` (bridge
|
|
9
9
|
* pairing) must keep working byte-for-byte unchanged, and cli_login has an
|
|
10
10
|
* outcome bridge pairing's loop never handles — the approval screen lets the
|
|
11
11
|
* human DENY a CLI login, and this CLI needs to say so plainly rather than
|
|
@@ -107,3 +107,17 @@ export declare function createLocalWorkspace(opts: BridgeApiOpts, params: {
|
|
|
107
107
|
export declare function deleteWorkspace(opts: BridgeApiOpts, workspaceId: string): Promise<{
|
|
108
108
|
success: boolean;
|
|
109
109
|
}>;
|
|
110
|
+
/**
|
|
111
|
+
* rival-10 — should this bridge forward its agents' file touches in
|
|
112
|
+
* `workspaceId`? Sends the workspace id only. `live` is true only for a ranked
|
|
113
|
+
* match workspace this user owns while the match is in play.
|
|
114
|
+
*/
|
|
115
|
+
export declare function getRivalTouchStatus(opts: BridgeApiOpts, workspaceId: string): Promise<{
|
|
116
|
+
live: boolean;
|
|
117
|
+
recheckMs: number;
|
|
118
|
+
}>;
|
|
119
|
+
/** rival-10 — forward live touches: agent id + file paths per touch, nothing else. */
|
|
120
|
+
export declare function postRivalTouches(opts: BridgeApiOpts, workspaceId: string, touches: Array<{
|
|
121
|
+
agentId: string;
|
|
122
|
+
filePaths: string[];
|
|
123
|
+
}>): Promise<void>;
|
package/dist/bridge/apiClient.js
CHANGED
|
@@ -8,6 +8,8 @@ exports.listDevices = listDevices;
|
|
|
8
8
|
exports.revokeDevice = revokeDevice;
|
|
9
9
|
exports.createLocalWorkspace = createLocalWorkspace;
|
|
10
10
|
exports.deleteWorkspace = deleteWorkspace;
|
|
11
|
+
exports.getRivalTouchStatus = getRivalTouchStatus;
|
|
12
|
+
exports.postRivalTouches = postRivalTouches;
|
|
11
13
|
class BridgeApiError extends Error {
|
|
12
14
|
code;
|
|
13
15
|
status;
|
|
@@ -25,12 +27,12 @@ exports.BridgeApiError = BridgeApiError;
|
|
|
25
27
|
* machines) comes back PAT_FORBIDDEN with a route-allowlist message that
|
|
26
28
|
* means nothing to an end user. Map it here, at the one place every bridge
|
|
27
29
|
* API error is constructed, so EVERY call site (registerDevice, MCP
|
|
28
|
-
* registration, local workspace create/delete, `
|
|
30
|
+
* registration, local workspace create/delete, `hiveop devices`, ...)
|
|
29
31
|
* gets the same actionable hint with no per-call-site plumbing.
|
|
30
32
|
*/
|
|
31
33
|
function errorMessageFor(code, rawMessage, status) {
|
|
32
34
|
if (code === 'PAT_FORBIDDEN') {
|
|
33
|
-
return 'your pairing is out of date — run `
|
|
35
|
+
return 'your pairing is out of date — run `hiveop pair` again';
|
|
34
36
|
}
|
|
35
37
|
return rawMessage || `HTTP ${status}`;
|
|
36
38
|
}
|
|
@@ -96,3 +98,18 @@ function createLocalWorkspace(opts, params) {
|
|
|
96
98
|
function deleteWorkspace(opts, workspaceId) {
|
|
97
99
|
return bridgeRequest(opts, 'DELETE', `/workspaces/${workspaceId}`);
|
|
98
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* rival-10 — should this bridge forward its agents' file touches in
|
|
103
|
+
* `workspaceId`? Sends the workspace id only. `live` is true only for a ranked
|
|
104
|
+
* match workspace this user owns while the match is in play.
|
|
105
|
+
*/
|
|
106
|
+
async function getRivalTouchStatus(opts, workspaceId) {
|
|
107
|
+
const json = await bridgeRequest(opts, 'GET', `/ranked/workspaces/${encodeURIComponent(workspaceId)}/touches`);
|
|
108
|
+
return { live: json.data?.live === true, recheckMs: Number(json.data?.recheckMs) || 30_000 };
|
|
109
|
+
}
|
|
110
|
+
/** rival-10 — forward live touches: agent id + file paths per touch, nothing else. */
|
|
111
|
+
async function postRivalTouches(opts, workspaceId, touches) {
|
|
112
|
+
await bridgeRequest(opts, 'POST', `/ranked/workspaces/${encodeURIComponent(workspaceId)}/touches`, {
|
|
113
|
+
touches: touches.map((t) => ({ agentId: t.agentId, filePaths: t.filePaths })),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bridge autostart (bct follow-up, 2026-07-30; PATH/pin fix, 2026-08-13) —
|
|
3
|
-
* `
|
|
3
|
+
* `hiveop bridge install`.
|
|
4
4
|
*
|
|
5
5
|
* Turns the one-liner into one-time-EVER setup: a macOS LaunchAgent starts
|
|
6
6
|
* the bridge at login and keeps it alive, so the Terminal pane always finds
|
package/dist/bridge/autostart.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* Bridge autostart (bct follow-up, 2026-07-30; PATH/pin fix, 2026-08-13) —
|
|
4
|
-
* `
|
|
4
|
+
* `hiveop bridge install`.
|
|
5
5
|
*
|
|
6
6
|
* Turns the one-liner into one-time-EVER setup: a macOS LaunchAgent starts
|
|
7
7
|
* the bridge at login and keeps it alive, so the Terminal pane always finds
|
|
@@ -38,9 +38,6 @@
|
|
|
38
38
|
* (Promise.all) per checkout, and why the whole scan is fire-and-forget from
|
|
39
39
|
* relayClient's point of view.
|
|
40
40
|
*/
|
|
41
|
-
/** `cmd args...` runner — same shape used across repoCheckout.ts /
|
|
42
|
-
* changesReport.ts's injected `run` (git invoked as `git -C <path> ...`,
|
|
43
|
-
* never via a shell cwd option, so this stays a flat argv runner). */
|
|
44
41
|
export type InventoryRun = (cmd: string, args: string[]) => Promise<{
|
|
45
42
|
stdout: string;
|
|
46
43
|
code: number;
|
|
@@ -63,37 +60,71 @@ export interface CheckoutInventoryDeps {
|
|
|
63
60
|
/** git runner, invoked as `run('git', ['-C', path, ...])` — see the module
|
|
64
61
|
* doc comment. */
|
|
65
62
|
run: InventoryRun;
|
|
63
|
+
/** Reads a checkout file (CLAUDE.md, for the managed-region check).
|
|
64
|
+
* Defaults to the real filesystem. */
|
|
65
|
+
readText?: ReadText;
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Inventory format version. 2 (cli 0.6.9) adds `dirtyKinds`/`noUpstream`, a
|
|
69
|
+
* dirty check that ignores the bridge's own CLAUDE.md region, and the
|
|
70
|
+
* `park-save` / `inventory-scan` frames. Stamped on EVERY entry so the arena
|
|
71
|
+
* and backend can tell a box that understands Save from one that does not —
|
|
72
|
+
* an old bridge would treat an unknown frame as an error, never as a park.
|
|
73
|
+
*/
|
|
74
|
+
export declare const INVENTORY_SCAN_VERSION = 2;
|
|
75
|
+
export type DirtyKind = 'uncommitted' | 'unpushed' | 'stashed';
|
|
67
76
|
export interface ParsedCheckoutEntry {
|
|
68
77
|
workspaceId: string;
|
|
69
78
|
unparseable: false;
|
|
70
79
|
bytes: number;
|
|
71
80
|
dirty: boolean;
|
|
72
81
|
mtimeMs: number;
|
|
82
|
+
/** Which of the three conditions made it dirty (empty when clean). */
|
|
83
|
+
dirtyKinds: DirtyKind[];
|
|
84
|
+
/** The branch tracks nothing (never pushed, or its remote branch is gone). */
|
|
85
|
+
noUpstream: boolean;
|
|
86
|
+
scanVersion: number;
|
|
73
87
|
}
|
|
74
88
|
export interface UnparseableCheckoutEntry {
|
|
75
89
|
workspaceId: string;
|
|
76
90
|
unparseable: true;
|
|
77
91
|
reason: string;
|
|
92
|
+
scanVersion: number;
|
|
78
93
|
}
|
|
79
94
|
export type CheckoutInventoryEntry = ParsedCheckoutEntry | UnparseableCheckoutEntry;
|
|
95
|
+
/** Reads a text file, or null when it cannot. Injected in tests. */
|
|
96
|
+
export type ReadText = (absPath: string) => string | null;
|
|
97
|
+
/** CLAUDE.md with the bridge's managed region (contextWriter.ts) cut out. */
|
|
98
|
+
export declare function stripManagedRegion(text: string): string;
|
|
99
|
+
export interface DirtyReport {
|
|
100
|
+
dirty: boolean;
|
|
101
|
+
kinds: DirtyKind[];
|
|
102
|
+
noUpstream: boolean;
|
|
103
|
+
}
|
|
80
104
|
/**
|
|
81
|
-
* PRD "Dirty means" section: uncommitted changes
|
|
82
|
-
*
|
|
83
|
-
* stashes (`git stash list` non-empty) — any ONE independently marks the
|
|
84
|
-
* checkout dirty. `status`/`stash` failing outright (non-zero exit) means the
|
|
85
|
-
* directory isn't a usable git checkout at all — 'unparseable', not clean.
|
|
86
|
-
* `rev-list @{u}..HEAD` failing is the ordinary "no upstream configured"
|
|
87
|
-
* case (a branch with nothing to compare against) and is treated as simply
|
|
88
|
-
* "not unpushed" rather than a parse failure — every real checkout has a
|
|
89
|
-
* working `status`/`stash`, so those two staying reliable is what actually
|
|
90
|
-
* distinguishes a healthy checkout from a broken one.
|
|
105
|
+
* PRD "Dirty means" section: uncommitted changes, unpushed commits, OR
|
|
106
|
+
* stashes — any ONE independently marks the checkout dirty.
|
|
91
107
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
108
|
+
* uncommitted — `git status --porcelain`, minus the bridge's own writes
|
|
109
|
+
* (isBridgeManagedChange).
|
|
110
|
+
* unpushed — commits on HEAD that no remote-tracking ref contains
|
|
111
|
+
* (`rev-list HEAD --not --remotes`). NOT `@{u}..HEAD`: that
|
|
112
|
+
* failed outright with no upstream, and counted work pushed
|
|
113
|
+
* to any other branch (a park branch included) as unpushed —
|
|
114
|
+
* so park-and-remove's own pre-delete re-check aborted every
|
|
115
|
+
* park that had committed something.
|
|
116
|
+
* stashed — `git stash list` non-empty.
|
|
117
|
+
*
|
|
118
|
+
* `status`/`stash` failing means the directory isn't a usable git checkout —
|
|
119
|
+
* 'unparseable', not clean.
|
|
120
|
+
*/
|
|
121
|
+
export declare function inspectDirty(path: string, run: InventoryRun, readText?: ReadText): Promise<DirtyReport | 'unparseable'>;
|
|
122
|
+
/**
|
|
123
|
+
* The boolean form — checkoutReclaim.ts's immediate-pre-delete re-check uses
|
|
124
|
+
* this EXACT definition rather than reimplementing it. One dirty definition,
|
|
125
|
+
* every caller.
|
|
95
126
|
*/
|
|
96
|
-
export declare function checkDirty(path: string, run: InventoryRun): Promise<boolean | 'unparseable'>;
|
|
127
|
+
export declare function checkDirty(path: string, run: InventoryRun, readText?: ReadText): Promise<boolean | 'unparseable'>;
|
|
97
128
|
/**
|
|
98
129
|
* PURE full-inventory scan. One record per checkout under `reposBaseDir`; a
|
|
99
130
|
* checkout that can't be read/parsed is reported as `unparseable: true`
|