@wenrwa/marketplace-cli 0.6.6 → 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-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.js +2 -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/server.d.ts +1 -1
- 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 +40 -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
|
});
|
|
@@ -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
|
package/dist/bridge/apiClient.js
CHANGED
|
@@ -27,12 +27,12 @@ exports.BridgeApiError = BridgeApiError;
|
|
|
27
27
|
* machines) comes back PAT_FORBIDDEN with a route-allowlist message that
|
|
28
28
|
* means nothing to an end user. Map it here, at the one place every bridge
|
|
29
29
|
* API error is constructed, so EVERY call site (registerDevice, MCP
|
|
30
|
-
* registration, local workspace create/delete, `
|
|
30
|
+
* registration, local workspace create/delete, `hiveop devices`, ...)
|
|
31
31
|
* gets the same actionable hint with no per-call-site plumbing.
|
|
32
32
|
*/
|
|
33
33
|
function errorMessageFor(code, rawMessage, status) {
|
|
34
34
|
if (code === 'PAT_FORBIDDEN') {
|
|
35
|
-
return 'your pairing is out of date — run `
|
|
35
|
+
return 'your pairing is out of date — run `hiveop pair` again';
|
|
36
36
|
}
|
|
37
37
|
return rawMessage || `HTTP ${status}`;
|
|
38
38
|
}
|
|
@@ -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`
|
|
@@ -39,41 +39,178 @@
|
|
|
39
39
|
* (Promise.all) per checkout, and why the whole scan is fire-and-forget from
|
|
40
40
|
* relayClient's point of view.
|
|
41
41
|
*/
|
|
42
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
43
|
+
if (k2 === undefined) k2 = k;
|
|
44
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
45
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
46
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
47
|
+
}
|
|
48
|
+
Object.defineProperty(o, k2, desc);
|
|
49
|
+
}) : (function(o, m, k, k2) {
|
|
50
|
+
if (k2 === undefined) k2 = k;
|
|
51
|
+
o[k2] = m[k];
|
|
52
|
+
}));
|
|
53
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
54
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
55
|
+
}) : function(o, v) {
|
|
56
|
+
o["default"] = v;
|
|
57
|
+
});
|
|
58
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
59
|
+
var ownKeys = function(o) {
|
|
60
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
61
|
+
var ar = [];
|
|
62
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
63
|
+
return ar;
|
|
64
|
+
};
|
|
65
|
+
return ownKeys(o);
|
|
66
|
+
};
|
|
67
|
+
return function (mod) {
|
|
68
|
+
if (mod && mod.__esModule) return mod;
|
|
69
|
+
var result = {};
|
|
70
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
71
|
+
__setModuleDefault(result, mod);
|
|
72
|
+
return result;
|
|
73
|
+
};
|
|
74
|
+
})();
|
|
42
75
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
76
|
+
exports.INVENTORY_SCAN_VERSION = void 0;
|
|
77
|
+
exports.stripManagedRegion = stripManagedRegion;
|
|
78
|
+
exports.inspectDirty = inspectDirty;
|
|
43
79
|
exports.checkDirty = checkDirty;
|
|
44
80
|
exports.scanCheckoutInventory = scanCheckoutInventory;
|
|
45
81
|
exports.createCheckoutInventoryReporter = createCheckoutInventoryReporter;
|
|
82
|
+
/** `cmd args...` runner — same shape used across repoCheckout.ts /
|
|
83
|
+
* changesReport.ts's injected `run` (git invoked as `git -C <path> ...`,
|
|
84
|
+
* never via a shell cwd option, so this stays a flat argv runner). */
|
|
85
|
+
const fs = __importStar(require("fs"));
|
|
86
|
+
const contextWriter_1 = require("./contextWriter");
|
|
87
|
+
/**
|
|
88
|
+
* Inventory format version. 2 (cli 0.6.9) adds `dirtyKinds`/`noUpstream`, a
|
|
89
|
+
* dirty check that ignores the bridge's own CLAUDE.md region, and the
|
|
90
|
+
* `park-save` / `inventory-scan` frames. Stamped on EVERY entry so the arena
|
|
91
|
+
* and backend can tell a box that understands Save from one that does not —
|
|
92
|
+
* an old bridge would treat an unknown frame as an error, never as a park.
|
|
93
|
+
*/
|
|
94
|
+
exports.INVENTORY_SCAN_VERSION = 2;
|
|
95
|
+
const defaultReadText = (absPath) => {
|
|
96
|
+
try {
|
|
97
|
+
return fs.readFileSync(absPath, 'utf8');
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
/** `run`, with a rejection folded into a non-zero exit. The production
|
|
104
|
+
* runner (commands/bridge.ts execRunner) REJECTS on a failed command with no
|
|
105
|
+
* stdout — the old dirty check let that escape, so a branch with no upstream
|
|
106
|
+
* surfaced as "Could not read this checkout: Command failed: git … rev-list
|
|
107
|
+
* @{u}..HEAD" instead of as a checkout with unpushed work. */
|
|
108
|
+
async function tryRun(run, args) {
|
|
109
|
+
try {
|
|
110
|
+
return await run('git', args);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
return { stdout: err instanceof Error ? err.message : String(err), code: 1 };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** CLAUDE.md with the bridge's managed region (contextWriter.ts) cut out. */
|
|
117
|
+
function stripManagedRegion(text) {
|
|
118
|
+
const start = text.indexOf(contextWriter_1.CONTEXT_START_MARKER);
|
|
119
|
+
const end = text.indexOf(contextWriter_1.CONTEXT_END_MARKER);
|
|
120
|
+
if (start === -1 || end === -1 || end < start)
|
|
121
|
+
return text.trim();
|
|
122
|
+
return (text.slice(0, start) + text.slice(end + contextWriter_1.CONTEXT_END_MARKER.length)).trim();
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Is this porcelain line the bridge's OWN write, not the user's work?
|
|
126
|
+
*
|
|
127
|
+
* The bridge rewrites a fenced region of `CLAUDE.md` every time a session
|
|
128
|
+
* opens (wmr-07) and keeps `.hiveop/` for itself. Counting either as "unsaved
|
|
129
|
+
* work" marked nearly every checkout dirty — owner report 2026-10-02: finished
|
|
130
|
+
* Practice match copies, all pushed, all "Unsaved work". Only the managed
|
|
131
|
+
* region is forgiven: any user text in CLAUDE.md still counts.
|
|
132
|
+
*/
|
|
133
|
+
async function isBridgeManagedChange(path, line, run, readText) {
|
|
134
|
+
const code = line.slice(0, 2);
|
|
135
|
+
const file = line.slice(3).trim().replace(/^"|"$/g, '');
|
|
136
|
+
if (file === '.hiveop' || file.startsWith('.hiveop/'))
|
|
137
|
+
return true;
|
|
138
|
+
if (file !== 'CLAUDE.md')
|
|
139
|
+
return false;
|
|
140
|
+
const working = readText(`${path}/CLAUDE.md`);
|
|
141
|
+
if (working === null)
|
|
142
|
+
return false;
|
|
143
|
+
const rest = stripManagedRegion(working);
|
|
144
|
+
if (code === '??')
|
|
145
|
+
return rest === '';
|
|
146
|
+
if (!/^[ M][ M]$/.test(code))
|
|
147
|
+
return false; // added/deleted/renamed — the user's
|
|
148
|
+
const head = await tryRun(run, ['-C', path, 'show', 'HEAD:CLAUDE.md']);
|
|
149
|
+
if (head.code !== 0)
|
|
150
|
+
return false;
|
|
151
|
+
return stripManagedRegion(head.stdout) === rest;
|
|
152
|
+
}
|
|
46
153
|
/**
|
|
47
|
-
* PRD "Dirty means" section: uncommitted changes
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
154
|
+
* PRD "Dirty means" section: uncommitted changes, unpushed commits, OR
|
|
155
|
+
* stashes — any ONE independently marks the checkout dirty.
|
|
156
|
+
*
|
|
157
|
+
* uncommitted — `git status --porcelain`, minus the bridge's own writes
|
|
158
|
+
* (isBridgeManagedChange).
|
|
159
|
+
* unpushed — commits on HEAD that no remote-tracking ref contains
|
|
160
|
+
* (`rev-list HEAD --not --remotes`). NOT `@{u}..HEAD`: that
|
|
161
|
+
* failed outright with no upstream, and counted work pushed
|
|
162
|
+
* to any other branch (a park branch included) as unpushed —
|
|
163
|
+
* so park-and-remove's own pre-delete re-check aborted every
|
|
164
|
+
* park that had committed something.
|
|
165
|
+
* stashed — `git stash list` non-empty.
|
|
57
166
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* conditions a second time. One dirty definition, two callers.
|
|
167
|
+
* `status`/`stash` failing means the directory isn't a usable git checkout —
|
|
168
|
+
* 'unparseable', not clean.
|
|
61
169
|
*/
|
|
62
|
-
async function
|
|
63
|
-
const status = await run
|
|
170
|
+
async function inspectDirty(path, run, readText = defaultReadText) {
|
|
171
|
+
const status = await tryRun(run, ['-C', path, 'status', '--porcelain']);
|
|
64
172
|
if (status.code !== 0)
|
|
65
173
|
return 'unparseable';
|
|
66
|
-
const stash = await run
|
|
174
|
+
const stash = await tryRun(run, ['-C', path, 'stash', 'list']);
|
|
67
175
|
if (stash.code !== 0)
|
|
68
176
|
return 'unparseable';
|
|
69
|
-
const
|
|
177
|
+
const lines = status.stdout.split('\n').filter((l) => l.trim().length > 0);
|
|
178
|
+
let uncommitted = false;
|
|
179
|
+
for (const line of lines) {
|
|
180
|
+
if (!(await isBridgeManagedChange(path, line, run, readText))) {
|
|
181
|
+
uncommitted = true;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const ahead = await tryRun(run, ['-C', path, 'rev-list', 'HEAD', '--not', '--remotes', '--count']);
|
|
186
|
+
// A repo with no commits at all has no HEAD — nothing to push.
|
|
70
187
|
const unpushed = ahead.code === 0 && parseInt(ahead.stdout.trim(), 10) > 0;
|
|
71
|
-
const
|
|
188
|
+
const upstream = await tryRun(run, ['-C', path, 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']);
|
|
72
189
|
const stashed = stash.stdout.trim().length > 0;
|
|
73
|
-
|
|
190
|
+
const kinds = [];
|
|
191
|
+
if (uncommitted)
|
|
192
|
+
kinds.push('uncommitted');
|
|
193
|
+
if (unpushed)
|
|
194
|
+
kinds.push('unpushed');
|
|
195
|
+
if (stashed)
|
|
196
|
+
kinds.push('stashed');
|
|
197
|
+
return { dirty: kinds.length > 0, kinds, noUpstream: upstream.code !== 0 };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The boolean form — checkoutReclaim.ts's immediate-pre-delete re-check uses
|
|
201
|
+
* this EXACT definition rather than reimplementing it. One dirty definition,
|
|
202
|
+
* every caller.
|
|
203
|
+
*/
|
|
204
|
+
async function checkDirty(path, run, readText) {
|
|
205
|
+
const r = await inspectDirty(path, run, readText);
|
|
206
|
+
return r === 'unparseable' ? r : r.dirty;
|
|
74
207
|
}
|
|
75
208
|
async function scanOne(reposBaseDir, workspaceId, deps, cache) {
|
|
76
209
|
const path = `${reposBaseDir}/${workspaceId}`;
|
|
210
|
+
const unparseable = (reason) => {
|
|
211
|
+
cache?.delete(workspaceId);
|
|
212
|
+
return { workspaceId, unparseable: true, reason, scanVersion: exports.INVENTORY_SCAN_VERSION };
|
|
213
|
+
};
|
|
77
214
|
let mtimeMs;
|
|
78
215
|
try {
|
|
79
216
|
mtimeMs = deps.statMtimeMs(path);
|
|
@@ -81,37 +218,35 @@ async function scanOne(reposBaseDir, workspaceId, deps, cache) {
|
|
|
81
218
|
catch {
|
|
82
219
|
mtimeMs = null;
|
|
83
220
|
}
|
|
84
|
-
if (mtimeMs === null)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
// re-scanned — cache keyed on mtime").
|
|
221
|
+
if (mtimeMs === null)
|
|
222
|
+
return unparseable('checkout directory is missing or unreadable');
|
|
223
|
+
// The SIZE is reused while the directory mtime is unchanged (PRD: "cache
|
|
224
|
+
// keyed on mtime" — `du` is the expensive half). The dirty state is NOT:
|
|
225
|
+
// a commit or a push lands inside .git and never touches the top-level
|
|
226
|
+
// mtime, so a cached dirty flag outlived the push that cleared it.
|
|
91
227
|
const cached = cache?.get(workspaceId);
|
|
92
|
-
if (cached && cached.mtimeMs === mtimeMs) {
|
|
93
|
-
return cached;
|
|
94
|
-
}
|
|
95
228
|
try {
|
|
96
229
|
const [bytes, dirty] = await Promise.all([
|
|
97
|
-
deps.sizeBytes(path),
|
|
98
|
-
|
|
230
|
+
cached && cached.mtimeMs === mtimeMs ? Promise.resolve(cached.bytes) : deps.sizeBytes(path),
|
|
231
|
+
inspectDirty(path, deps.run, deps.readText),
|
|
99
232
|
]);
|
|
100
|
-
if (dirty === 'unparseable')
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
233
|
+
if (dirty === 'unparseable')
|
|
234
|
+
return unparseable('git status failed — not a usable checkout');
|
|
235
|
+
const entry = {
|
|
236
|
+
workspaceId,
|
|
237
|
+
unparseable: false,
|
|
238
|
+
bytes,
|
|
239
|
+
dirty: dirty.dirty,
|
|
240
|
+
mtimeMs,
|
|
241
|
+
dirtyKinds: dirty.kinds,
|
|
242
|
+
noUpstream: dirty.noUpstream,
|
|
243
|
+
scanVersion: exports.INVENTORY_SCAN_VERSION,
|
|
244
|
+
};
|
|
105
245
|
cache?.set(workspaceId, entry);
|
|
106
246
|
return entry;
|
|
107
247
|
}
|
|
108
248
|
catch (err) {
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
workspaceId,
|
|
112
|
-
unparseable: true,
|
|
113
|
-
reason: err instanceof Error ? err.message : String(err),
|
|
114
|
-
};
|
|
249
|
+
return unparseable(err instanceof Error ? err.message : String(err));
|
|
115
250
|
}
|
|
116
251
|
}
|
|
117
252
|
/**
|
|
@@ -149,10 +284,17 @@ async function scanCheckoutInventory(reposBaseDir, deps, cache) {
|
|
|
149
284
|
*/
|
|
150
285
|
function createCheckoutInventoryReporter(opts) {
|
|
151
286
|
let inFlight = false;
|
|
287
|
+
// A trigger that lands mid-scan (a Save finishing, the arena asking for a
|
|
288
|
+
// fresh report) is remembered and run ONCE after the current pass — the
|
|
289
|
+
// pass in flight may have read the checkout before the change, and dropping
|
|
290
|
+
// the request left the Storage view showing the old state for good.
|
|
291
|
+
let again = false;
|
|
152
292
|
const cache = new Map();
|
|
153
293
|
function trigger() {
|
|
154
|
-
if (inFlight)
|
|
155
|
-
|
|
294
|
+
if (inFlight) {
|
|
295
|
+
again = true;
|
|
296
|
+
return;
|
|
297
|
+
} // not restarted — queued once
|
|
156
298
|
inFlight = true;
|
|
157
299
|
scanCheckoutInventory(opts.reposBaseDir, opts.deps, cache)
|
|
158
300
|
.then((checkouts) => {
|
|
@@ -163,6 +305,10 @@ function createCheckoutInventoryReporter(opts) {
|
|
|
163
305
|
})
|
|
164
306
|
.finally(() => {
|
|
165
307
|
inFlight = false;
|
|
308
|
+
if (again) {
|
|
309
|
+
again = false;
|
|
310
|
+
trigger();
|
|
311
|
+
}
|
|
166
312
|
});
|
|
167
313
|
}
|
|
168
314
|
return { trigger };
|