@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.
Files changed (74) hide show
  1. package/README.md +85 -85
  2. package/dist/__tests__/auth-login-core.test.js +2 -2
  3. package/dist/__tests__/auth.test.js +4 -4
  4. package/dist/__tests__/bridge-autostart.test.js +9 -9
  5. package/dist/__tests__/bridge-checkout-dirty-real-git.test.d.ts +1 -0
  6. package/dist/__tests__/bridge-checkout-dirty-real-git.test.js +145 -0
  7. package/dist/__tests__/bridge-checkout-inventory-reporter.test.js +6 -2
  8. package/dist/__tests__/bridge-checkout-inventory.test.js +128 -14
  9. package/dist/__tests__/bridge-checkout-park-and-remove.test.js +57 -8
  10. package/dist/__tests__/bridge-claude-trust.test.d.ts +1 -0
  11. package/dist/__tests__/bridge-claude-trust.test.js +137 -0
  12. package/dist/__tests__/bridge-command-browser-options.test.js +1 -1
  13. package/dist/__tests__/bridge-command-install-env.test.js +1 -1
  14. package/dist/__tests__/bridge-device-registration.test.js +1 -1
  15. package/dist/__tests__/bridge-folder-registry.test.js +1 -1
  16. package/dist/__tests__/bridge-local-add-core.test.js +1 -1
  17. package/dist/__tests__/bridge-match-pointer.test.d.ts +1 -0
  18. package/dist/__tests__/bridge-match-pointer.test.js +136 -0
  19. package/dist/__tests__/bridge-pairing-api.test.js +1 -1
  20. package/dist/__tests__/bridge-pairing-core.test.js +1 -1
  21. package/dist/__tests__/bridge-park-remove-frame.test.js +61 -0
  22. package/dist/__tests__/bridge-relay-client-checkout-inventory.test.js +1 -1
  23. package/dist/__tests__/bridge-relay-mode.test.js +1 -1
  24. package/dist/__tests__/bridge-server.test.js +1 -1
  25. package/dist/__tests__/config.test.js +1 -1
  26. package/dist/__tests__/devices.test.js +1 -1
  27. package/dist/__tests__/local.test.js +1 -1
  28. package/dist/__tests__/openBrowser.test.js +1 -1
  29. package/dist/__tests__/websocket.test.js +3 -3
  30. package/dist/auth/identity.d.ts +1 -1
  31. package/dist/auth/login.d.ts +2 -2
  32. package/dist/bridge/apiClient.js +2 -2
  33. package/dist/bridge/autostart.d.ts +1 -1
  34. package/dist/bridge/autostart.js +1 -1
  35. package/dist/bridge/checkoutInventory.d.ts +48 -17
  36. package/dist/bridge/checkoutInventory.js +190 -44
  37. package/dist/bridge/checkoutParkAndRemove.d.ts +22 -1
  38. package/dist/bridge/checkoutParkAndRemove.js +37 -7
  39. package/dist/bridge/claudeTrust.d.ts +37 -0
  40. package/dist/bridge/claudeTrust.js +85 -0
  41. package/dist/bridge/config.d.ts +1 -1
  42. package/dist/bridge/contextWriter.d.ts +2 -0
  43. package/dist/bridge/contextWriter.js +3 -2
  44. package/dist/bridge/deviceRegistration.d.ts +1 -1
  45. package/dist/bridge/envSanitize.d.ts +1 -1
  46. package/dist/bridge/envSanitize.js +1 -1
  47. package/dist/bridge/folderRegistry.js +1 -1
  48. package/dist/bridge/localAdd.js +1 -1
  49. package/dist/bridge/localContextWriter.d.ts +2 -0
  50. package/dist/bridge/localContextWriter.js +3 -2
  51. package/dist/bridge/memoryBrief.d.ts +16 -4
  52. package/dist/bridge/memoryBrief.js +17 -9
  53. package/dist/bridge/pairing.d.ts +1 -1
  54. package/dist/bridge/relayClient.js +11 -1
  55. package/dist/bridge/server.d.ts +1 -1
  56. package/dist/bridge/workGraphPointer.d.ts +19 -0
  57. package/dist/bridge/workGraphPointer.js +26 -1
  58. package/dist/commands/apps.js +1 -1
  59. package/dist/commands/auth.d.ts +1 -1
  60. package/dist/commands/auth.js +6 -6
  61. package/dist/commands/bridge.js +40 -13
  62. package/dist/commands/credits.js +1 -1
  63. package/dist/commands/devices.d.ts +1 -1
  64. package/dist/commands/devices.js +4 -4
  65. package/dist/commands/local.d.ts +1 -1
  66. package/dist/commands/local.js +5 -5
  67. package/dist/commands/pair.d.ts +1 -1
  68. package/dist/commands/pair.js +4 -4
  69. package/dist/index.js +2 -2
  70. package/dist/utils/config.js +2 -2
  71. package/dist/utils/openBrowser.d.ts +1 -1
  72. package/dist/utils/openBrowser.js +1 -1
  73. package/dist/utils/websocket.js +1 -1
  74. 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 `wen-market bridge --relay`', () => {
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 `wen-market local add` mutating the registry mid-run.
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('wen-market auth login');
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: `wen-market devices` list/revoke. Uses this machine's own
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: `wen-market local add/list/remove`. Folder→workspace
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: `wen-market auth login` tries to open the approval URL in a
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(/wen-market auth login/);
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(/wen-market auth login/);
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(/wen-market auth login/);
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', () => {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * wha-08 — the one authenticated call `wen-market auth login` (device flow,
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.
@@ -1,11 +1,11 @@
1
1
  /**
2
- * wha-08 — `wen-market auth login`: CLI browser login. Device-code pairing
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: `wen-market pair` (bridge
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
@@ -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, `wen-market devices`, ...)
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 `wen-market pair` again';
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
- * `wen-market bridge install`.
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
@@ -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
- * `wen-market bridge install`.
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 (`git status --porcelain`
82
- * non-empty), unpushed commits (`git rev-list @{u}..HEAD --count` > 0), OR
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
- * Exported (hcs-05) — checkoutReclaim.ts reuses this EXACT function for its
93
- * immediate-pre-delete dirty re-check, rather than reimplementing the three
94
- * conditions a second time. One dirty definition, two callers.
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 (`git status --porcelain`
48
- * non-empty), unpushed commits (`git rev-list @{u}..HEAD --count` > 0), OR
49
- * stashes (`git stash list` non-empty) — any ONE independently marks the
50
- * checkout dirty. `status`/`stash` failing outright (non-zero exit) means the
51
- * directory isn't a usable git checkout at all — 'unparseable', not clean.
52
- * `rev-list @{u}..HEAD` failing is the ordinary "no upstream configured"
53
- * case (a branch with nothing to compare against) and is treated as simply
54
- * "not unpushed" rather than a parse failure — every real checkout has a
55
- * working `status`/`stash`, so those two staying reliable is what actually
56
- * distinguishes a healthy checkout from a broken one.
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
- * Exported (hcs-05) — checkoutReclaim.ts reuses this EXACT function for its
59
- * immediate-pre-delete dirty re-check, rather than reimplementing the three
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 checkDirty(path, run) {
63
- const status = await run('git', ['-C', path, 'status', '--porcelain']);
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('git', ['-C', path, 'stash', 'list']);
174
+ const stash = await tryRun(run, ['-C', path, 'stash', 'list']);
67
175
  if (stash.code !== 0)
68
176
  return 'unparseable';
69
- const ahead = await run('git', ['-C', path, 'rev-list', '@{u}..HEAD', '--count']);
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 uncommitted = status.stdout.trim().length > 0;
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
- return uncommitted || unpushed || stashed;
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
- cache?.delete(workspaceId);
86
- return { workspaceId, unparseable: true, reason: 'checkout directory is missing or unreadable' };
87
- }
88
- // Unchanged since the last scan — reuse the cached entry rather than
89
- // re-running du/git for it (PRD: "Unchanged checkouts should not be
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
- checkDirty(path, deps.run),
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
- cache?.delete(workspaceId);
102
- return { workspaceId, unparseable: true, reason: 'git status failed — not a usable checkout' };
103
- }
104
- const entry = { workspaceId, unparseable: false, bytes, dirty, mtimeMs };
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
- cache?.delete(workspaceId);
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
- return; // already running — not restarted
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 };