@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
|
@@ -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 };
|
|
@@ -37,6 +37,19 @@
|
|
|
37
37
|
* against the same working tree would race git itself, not merely the
|
|
38
38
|
* delete — hcs-05's set alone only covers the delete step.
|
|
39
39
|
*
|
|
40
|
+
* SAVE (cli 0.6.9, owner request "why can't I get these things to save
|
|
41
|
+
* without removing?"): `park(id, onPhase, { remove: false })` runs the same
|
|
42
|
+
* commit -> push -> verify and stops there — outcome `saved`, checkout kept.
|
|
43
|
+
* Remove on a checkout with nothing to park now removes it (through the
|
|
44
|
+
* reclaimer, which re-checks clean + unattached first): everything in it is
|
|
45
|
+
* already on a remote, so refusing left clean match copies with no way out
|
|
46
|
+
* but the 14-day automatic sweep.
|
|
47
|
+
*
|
|
48
|
+
* After a verified push the parking branch is recorded as a remote-tracking
|
|
49
|
+
* ref (`refs/remotes/origin/park/<id>`). The clone is `--single-branch`, so
|
|
50
|
+
* git does not do this itself, and without it the dirty check (commits no
|
|
51
|
+
* remote ref contains) could never see that the work is now saved.
|
|
52
|
+
*
|
|
40
53
|
* PHASES: `committing` / `pushing` / `removing`, reported through the
|
|
41
54
|
* optional `onPhase` callback as each begins — the wire layer
|
|
42
55
|
* (relayClient.ts) turns these into `park-remove-progress` frames so a UI
|
|
@@ -55,6 +68,10 @@ export type ParkAndRemoveOutcome = {
|
|
|
55
68
|
workspaceId: string;
|
|
56
69
|
outcome: 'nothing-to-park';
|
|
57
70
|
branch: string;
|
|
71
|
+
} | {
|
|
72
|
+
workspaceId: string;
|
|
73
|
+
outcome: 'saved';
|
|
74
|
+
branch: string;
|
|
58
75
|
} | {
|
|
59
76
|
workspaceId: string;
|
|
60
77
|
outcome: 'retained';
|
|
@@ -87,13 +104,17 @@ export interface CheckoutParkAndRemoveDeps {
|
|
|
87
104
|
* hcs-05 already proved. */
|
|
88
105
|
reclaimer: CheckoutReclaimer;
|
|
89
106
|
}
|
|
107
|
+
export interface ParkOptions {
|
|
108
|
+
/** false = Save: back the work up and keep the checkout. Default true. */
|
|
109
|
+
remove?: boolean;
|
|
110
|
+
}
|
|
90
111
|
export interface CheckoutParker {
|
|
91
112
|
/** Park (commit + push + remove-on-verified-push) one checkout.
|
|
92
113
|
* `onPhase`, when given, is called once per phase as it BEGINS —
|
|
93
114
|
* 'committing' first, always; 'pushing' and 'removing' only if the
|
|
94
115
|
* operation reaches them. Never throws — every failure mode is a typed
|
|
95
116
|
* outcome, same discipline as CheckoutReclaimer#reclaim. */
|
|
96
|
-
park(workspaceId: string, onPhase?: (phase: ParkPhase) => void): Promise<ParkAndRemoveOutcome>;
|
|
117
|
+
park(workspaceId: string, onPhase?: (phase: ParkPhase) => void, opts?: ParkOptions): Promise<ParkAndRemoveOutcome>;
|
|
97
118
|
}
|
|
98
119
|
/**
|
|
99
120
|
* Deterministic from the workspace id alone (no timestamp) — PURE, so a
|
|
@@ -41,6 +41,19 @@ exports.createCheckoutParker = createCheckoutParker;
|
|
|
41
41
|
* against the same working tree would race git itself, not merely the
|
|
42
42
|
* delete — hcs-05's set alone only covers the delete step.
|
|
43
43
|
*
|
|
44
|
+
* SAVE (cli 0.6.9, owner request "why can't I get these things to save
|
|
45
|
+
* without removing?"): `park(id, onPhase, { remove: false })` runs the same
|
|
46
|
+
* commit -> push -> verify and stops there — outcome `saved`, checkout kept.
|
|
47
|
+
* Remove on a checkout with nothing to park now removes it (through the
|
|
48
|
+
* reclaimer, which re-checks clean + unattached first): everything in it is
|
|
49
|
+
* already on a remote, so refusing left clean match copies with no way out
|
|
50
|
+
* but the 14-day automatic sweep.
|
|
51
|
+
*
|
|
52
|
+
* After a verified push the parking branch is recorded as a remote-tracking
|
|
53
|
+
* ref (`refs/remotes/origin/park/<id>`). The clone is `--single-branch`, so
|
|
54
|
+
* git does not do this itself, and without it the dirty check (commits no
|
|
55
|
+
* remote ref contains) could never see that the work is now saved.
|
|
56
|
+
*
|
|
44
57
|
* PHASES: `committing` / `pushing` / `removing`, reported through the
|
|
45
58
|
* optional `onPhase` callback as each begins — the wire layer
|
|
46
59
|
* (relayClient.ts) turns these into `park-remove-progress` frames so a UI
|
|
@@ -81,7 +94,8 @@ function createCheckoutParker(deps) {
|
|
|
81
94
|
// must cover the WHOLE operation (commit+push+remove) and not merely the
|
|
82
95
|
// delete step, which is a SEPARATE lock (hcs-05's reclaimer, reused below).
|
|
83
96
|
const inFlight = new Set();
|
|
84
|
-
async function park(workspaceId, onPhase) {
|
|
97
|
+
async function park(workspaceId, onPhase, opts = {}) {
|
|
98
|
+
const remove = opts.remove !== false;
|
|
85
99
|
// Path safety — resolved from workspaceId ALONE, reusing hcs-05's exact
|
|
86
100
|
// resolver rather than a second implementation. There is no path on the
|
|
87
101
|
// wire to trust in the first place.
|
|
@@ -132,14 +146,25 @@ function createCheckoutParker(deps) {
|
|
|
132
146
|
// A failure here (no upstream configured, detached HEAD, ...) is NOT
|
|
133
147
|
// a hard failure — it just means "assume there might be something to
|
|
134
148
|
// push" and let the push step itself be the source of truth.
|
|
135
|
-
|
|
149
|
+
// Commits no remote-tracking ref contains — the same definition the
|
|
150
|
+
// dirty check uses (checkoutInventory.ts inspectDirty).
|
|
151
|
+
const ahead = await runGit(deps.run, target, ['rev-list', 'HEAD', '--not', '--remotes', '--count']);
|
|
136
152
|
const hasUnpushed = ahead.code === 0 ? parseInt(ahead.stdout.trim(), 10) > 0 : true;
|
|
137
153
|
if (!committedSomething && !hasUnpushed) {
|
|
138
|
-
// Genuinely nothing to park
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
154
|
+
// Genuinely nothing to park: everything here is already on a remote.
|
|
155
|
+
// Save reports that. Remove goes straight to the reclaimer, whose
|
|
156
|
+
// own immediate re-check (clean, unattached) still guards the delete.
|
|
157
|
+
if (!remove)
|
|
158
|
+
return { workspaceId, outcome: 'nothing-to-park', branch };
|
|
159
|
+
phase = 'removing';
|
|
160
|
+
onPhase?.(phase);
|
|
161
|
+
const [cleanResult] = await deps.reclaimer.reclaim([
|
|
162
|
+
{ workspaceId, verdict: 'RECLAIM', reason: 'park-and-remove: nothing unsaved' },
|
|
163
|
+
]);
|
|
164
|
+
if (cleanResult.outcome === 'removed') {
|
|
165
|
+
return { workspaceId, outcome: 'removed', branch, bytesFreed: cleanResult.bytesFreed };
|
|
166
|
+
}
|
|
167
|
+
return { workspaceId, outcome: 'retained', branch, phase, reason: cleanResult.reason };
|
|
143
168
|
}
|
|
144
169
|
// ---- PUSHING --------------------------------------------------------
|
|
145
170
|
phase = 'pushing';
|
|
@@ -167,6 +192,11 @@ function createCheckoutParker(deps) {
|
|
|
167
192
|
if (remoteSha !== localSha) {
|
|
168
193
|
return { workspaceId, outcome: 'retained', branch, phase, reason: `push reported success but the remote ref points at a different commit (${remoteSha.slice(0, 8)} != ${localSha.slice(0, 8)}) — refusing to trust it` };
|
|
169
194
|
}
|
|
195
|
+
// Record what the remote just proved, so the dirty check sees the work
|
|
196
|
+
// as saved. Best-effort: the push is already verified either way.
|
|
197
|
+
await runGit(deps.run, target, ['update-ref', `refs/remotes/origin/${branch}`, localSha]);
|
|
198
|
+
if (!remove)
|
|
199
|
+
return { workspaceId, outcome: 'saved', branch };
|
|
170
200
|
// ---- REMOVING -------------------------------------------------------
|
|
171
201
|
// The work is now PROVABLY on the remote. Only past this point may
|
|
172
202
|
// anything be deleted, and only via hcs-05's own executor — see the
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playtest 2026-10-01 (Qubit Forge, hosted lane) — pre-trust the workspace
|
|
3
|
+
* checkouts this bridge itself clones, so Claude Code never opens on its
|
|
4
|
+
* folder-trust dialog in them.
|
|
5
|
+
*
|
|
6
|
+
* Claude Code (2.1.28x) shows "Quick safety check: Is this a project you
|
|
7
|
+
* created or one you trust? … ❯ No, exit / Yes, I trust this folder" on any
|
|
8
|
+
* folder its config has not trusted — EVEN under --dangerously-skip-
|
|
9
|
+
* permissions — and the default is "No, exit". Every match / workspace gets a
|
|
10
|
+
* fresh /data/repos/<workspaceId>, so every first agent there met it, and a
|
|
11
|
+
* queued first message (Send to agent) answered it with the default: Claude
|
|
12
|
+
* exited to a bare shell.
|
|
13
|
+
*
|
|
14
|
+
* The container is ours, the bridge cloned the folder from the workspace's own
|
|
15
|
+
* repo, and the agent was launched in bypass mode by the pilot's own spawn
|
|
16
|
+
* choice — the trust question has nobody meaningful to ask. So the bridge
|
|
17
|
+
* records the answer Claude would: `projects[<dir>].hasTrustDialogAccepted =
|
|
18
|
+
* true` in `$CLAUDE_CONFIG_DIR/.claude.json` (where Claude keeps its global
|
|
19
|
+
* config when CLAUDE_CONFIG_DIR is set — verified against 2.1.286).
|
|
20
|
+
*
|
|
21
|
+
* The entry must name the checkout itself: inside a git repo Claude only walks
|
|
22
|
+
* up to the repo ROOT looking for a trusted entry, so trusting /data/repos
|
|
23
|
+
* would not count.
|
|
24
|
+
*
|
|
25
|
+
* Scope: HOSTED LANE ONLY (commands/bridge.ts wires it into the container's
|
|
26
|
+
* clone-on-demand path, and only when CLAUDE_CONFIG_DIR is set), and only for
|
|
27
|
+
* folders strictly inside the bridge's repos dir. A local-lane user's own
|
|
28
|
+
* Claude config is never touched.
|
|
29
|
+
*
|
|
30
|
+
* Never clobbers: an unparseable config (it also carries onboarding/login
|
|
31
|
+
* state) is left alone; every other key is preserved; the write is tmp+rename.
|
|
32
|
+
*/
|
|
33
|
+
export type ClaudeTrustResult = 'trusted' | 'already' | 'skipped';
|
|
34
|
+
export declare function trustClaudeProjectDir(dir: string, opts: {
|
|
35
|
+
configDir: string;
|
|
36
|
+
reposBaseDir: string;
|
|
37
|
+
}): ClaudeTrustResult;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.trustClaudeProjectDir = trustClaudeProjectDir;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
function trustClaudeProjectDir(dir, opts) {
|
|
40
|
+
const base = path.resolve(opts.reposBaseDir);
|
|
41
|
+
const target = path.resolve(dir);
|
|
42
|
+
if (!target.startsWith(base + path.sep))
|
|
43
|
+
return 'skipped';
|
|
44
|
+
const file = path.join(opts.configDir, '.claude.json');
|
|
45
|
+
let cfg = {};
|
|
46
|
+
let raw = null;
|
|
47
|
+
try {
|
|
48
|
+
raw = fs.readFileSync(file, 'utf-8');
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
raw = null; // no config yet — Claude merges its defaults over what we write
|
|
52
|
+
}
|
|
53
|
+
if (raw !== null) {
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(raw);
|
|
56
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
57
|
+
return 'skipped';
|
|
58
|
+
cfg = parsed;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return 'skipped';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const projects = cfg.projects && typeof cfg.projects === 'object' && !Array.isArray(cfg.projects)
|
|
65
|
+
? cfg.projects
|
|
66
|
+
: {};
|
|
67
|
+
const entry = projects[target] && typeof projects[target] === 'object' ? projects[target] : {};
|
|
68
|
+
if (entry.hasTrustDialogAccepted === true)
|
|
69
|
+
return 'already';
|
|
70
|
+
const next = { ...cfg, projects: { ...projects, [target]: { ...entry, hasTrustDialogAccepted: true } } };
|
|
71
|
+
fs.mkdirSync(opts.configDir, { recursive: true });
|
|
72
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
73
|
+
try {
|
|
74
|
+
fs.writeFileSync(tmp, JSON.stringify(next, null, 2) + '\n', { mode: 0o600 });
|
|
75
|
+
fs.renameSync(tmp, file);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
try {
|
|
79
|
+
fs.rmSync(tmp, { force: true });
|
|
80
|
+
}
|
|
81
|
+
catch { /* best effort */ }
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
return 'trusted';
|
|
85
|
+
}
|
package/dist/bridge/config.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface BridgeConfig {
|
|
2
2
|
connectToken: string;
|
|
3
3
|
/** lws-03: set once this machine auto-registers as a device (first
|
|
4
|
-
* `
|
|
4
|
+
* `hiveop bridge` run) — persisted so re-registration is idempotent. */
|
|
5
5
|
deviceId?: string;
|
|
6
6
|
}
|
|
7
7
|
export interface DiscoveryInfo {
|
|
@@ -32,6 +32,8 @@ export declare function writeContext(input: {
|
|
|
32
32
|
/** Absent only for a pointers-only refresh (wmr-07). */
|
|
33
33
|
content?: string;
|
|
34
34
|
workGraphAvailable?: boolean;
|
|
35
|
+
/** 2026-10-02 — a match workspace: MATCH_POINTER replaces the work-queue pointer (workSection). */
|
|
36
|
+
match?: boolean;
|
|
35
37
|
/**
|
|
36
38
|
* wmr-01 — whether to carry the workspace-memory guidance. Decided by the
|
|
37
39
|
* caller on the same signal as workGraphAvailable (WENRWA_CONNECT_TOKEN),
|
|
@@ -148,8 +148,9 @@ function writeContext(input, deps) {
|
|
|
148
148
|
return existing.slice(s0 + exports.CONTEXT_START_MARKER.length, e0).trim();
|
|
149
149
|
})();
|
|
150
150
|
const pointers = [];
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
const work = (0, workGraphPointer_1.workSection)(input);
|
|
152
|
+
if (work)
|
|
153
|
+
pointers.push(work);
|
|
153
154
|
if (input.memoryAvailable)
|
|
154
155
|
pointers.push((0, memoryPointer_1.memorySection)(input.memoryBrief));
|
|
155
156
|
// wmr-07: a pointers-only refresh keeps the region's existing file content
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lws-03 — "Connect this machine": on first `
|
|
2
|
+
* lws-03 — "Connect this machine": on first `hiveop bridge` run with a
|
|
3
3
|
* Connect Token configured, auto-register the device and persist its id
|
|
4
4
|
* into bridge-config.json so re-registration is skipped thereafter (a
|
|
5
5
|
* pure resolver, mirroring resolveRelayMode in commands/bridge.ts, so the
|
|
@@ -20,7 +20,7 @@ export declare const STRIPPED_ENV_PREFIXES: readonly ["CLAUDE_CODE_"];
|
|
|
20
20
|
* fcct-27 — the RELAY CREDENTIALS are stripped here too, for a different and
|
|
21
21
|
* sharper reason than the session markers above.
|
|
22
22
|
*
|
|
23
|
-
* `
|
|
23
|
+
* `hiveop bridge` reads `CREW_RELAY_TOKEN` / `RELAY_TOKEN` from its own
|
|
24
24
|
* environment (the launch agent sets them), and every PTY it spawns inherits
|
|
25
25
|
* that environment. Seen live 2026-08-16: a crew member's `claude` process had
|
|
26
26
|
* the device-bound connect token sitting in plain view in `ps eww`. Nothing
|
|
@@ -24,7 +24,7 @@ exports.STRIPPED_ENV_PREFIXES = ['CLAUDE_CODE_'];
|
|
|
24
24
|
* fcct-27 — the RELAY CREDENTIALS are stripped here too, for a different and
|
|
25
25
|
* sharper reason than the session markers above.
|
|
26
26
|
*
|
|
27
|
-
* `
|
|
27
|
+
* `hiveop bridge` reads `CREW_RELAY_TOKEN` / `RELAY_TOKEN` from its own
|
|
28
28
|
* environment (the launch agent sets them), and every PTY it spawns inherits
|
|
29
29
|
* that environment. Seen live 2026-08-16: a crew member's `claude` process had
|
|
30
30
|
* the device-bound connect token sitting in plain view in `ps eww`. Nothing
|
|
@@ -41,7 +41,7 @@ exports.removeEntryByWorkspaceId = removeEntryByWorkspaceId;
|
|
|
41
41
|
exports.findEntryByPath = findEntryByPath;
|
|
42
42
|
exports.findEntryByWorkspaceId = findEntryByWorkspaceId;
|
|
43
43
|
/**
|
|
44
|
-
* lws-03 — folder→workspace registry. Backs `
|
|
44
|
+
* lws-03 — folder→workspace registry. Backs `hiveop local add/list/
|
|
45
45
|
* remove` and the discovery advertisement (server.ts). Lives alongside the
|
|
46
46
|
* bridge's other dotfiles under ~/.wen-market/ rather than a second dotdir,
|
|
47
47
|
* matching config.ts's convention. File-backed so it survives daemon
|
package/dist/bridge/localAdd.js
CHANGED
|
@@ -36,7 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.addLocalWorkspaceCore = addLocalWorkspaceCore;
|
|
37
37
|
/**
|
|
38
38
|
* lws-05 — bridge-side idempotent add core, behind POST /local/add. Mirrors
|
|
39
|
-
* `
|
|
39
|
+
* `hiveop local add`'s behavior (commands/local.ts) so the arena
|
|
40
40
|
* create-modal's Browse/typed-path attach mechanisms (PRD D3b/D3c) end at the
|
|
41
41
|
* exact same registered-workspace outcome as the CLI mechanism (D3a) —
|
|
42
42
|
* deps-injected so server.ts can wire it without this module knowing about
|
|
@@ -29,6 +29,8 @@ export declare function writeLocalContext(input: {
|
|
|
29
29
|
/** Absent only for a pointers-only refresh (wmr-07). */
|
|
30
30
|
content?: string;
|
|
31
31
|
workGraphAvailable?: boolean;
|
|
32
|
+
/** 2026-10-02 — a match workspace: MATCH_POINTER replaces the work-queue pointer (workSection). */
|
|
33
|
+
match?: boolean;
|
|
32
34
|
/** wmr-04 — two-lane parity with contextWriter.ts. Gated by the caller on
|
|
33
35
|
* the local lane's own honest signal (did THIS bridge run register the
|
|
34
36
|
* MCP server), not on a container env var that does not exist here. */
|
|
@@ -89,8 +89,9 @@ function writeLocalContext(input, deps) {
|
|
|
89
89
|
// this would be a two-lane bug nobody would notice until a local pilot
|
|
90
90
|
// reported missing memory.
|
|
91
91
|
const pointers = [];
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
const work = (0, workGraphPointer_1.workSection)(input);
|
|
93
|
+
if (work)
|
|
94
|
+
pointers.push(work);
|
|
94
95
|
if (input.memoryAvailable)
|
|
95
96
|
pointers.push((0, memoryPointer_1.memorySection)(input.memoryBrief));
|
|
96
97
|
// wmr-07 — the whole local file is bridge-owned, so "preserve the pushed
|