@remixhq/core 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +29 -0
- package/dist/api.js +1 -1
- package/dist/binding.d.ts +9 -4
- package/dist/binding.js +3 -1
- package/dist/chunk-4L3ZBZUQ.js +281 -0
- package/dist/chunk-BNKPTE2U.js +401 -0
- package/dist/chunk-C5NBNU32.js +240 -0
- package/dist/chunk-DXCL6I4Q.js +399 -0
- package/dist/chunk-HZNEDSRS.js +0 -0
- package/dist/chunk-IXWQWFYT.js +342 -0
- package/dist/chunk-K54U353Z.js +691 -0
- package/dist/chunk-RM2BGDBB.js +400 -0
- package/dist/chunk-ZXP6ENQY.js +244 -0
- package/dist/collab.d.ts +49 -7
- package/dist/collab.js +1071 -139
- package/dist/index.js +1 -1
- package/dist/repo.js +2 -1
- package/package.json +1 -1
package/dist/collab.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getCollabBindingPath,
|
|
3
3
|
readCollabBinding,
|
|
4
|
+
readCollabBindingState,
|
|
4
5
|
reserveAvailableDirPath,
|
|
5
6
|
reserveDirectory,
|
|
6
|
-
writeCollabBinding
|
|
7
|
-
|
|
7
|
+
writeCollabBinding,
|
|
8
|
+
writeCollabBindingSnapshot
|
|
9
|
+
} from "./chunk-IXWQWFYT.js";
|
|
10
|
+
import "./chunk-HZNEDSRS.js";
|
|
8
11
|
import {
|
|
9
12
|
assertRepoSnapshotUnchanged,
|
|
10
13
|
buildRepoFingerprint,
|
|
@@ -38,7 +41,7 @@ import {
|
|
|
38
41
|
summarizeUnifiedDiff,
|
|
39
42
|
validateUnifiedDiff,
|
|
40
43
|
writeTempUnifiedDiffBackup
|
|
41
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-RREREIGW.js";
|
|
42
45
|
import {
|
|
43
46
|
REMIX_ERROR_CODES
|
|
44
47
|
} from "./chunk-GC2MOT3U.js";
|
|
@@ -55,29 +58,29 @@ function describeBranch(value) {
|
|
|
55
58
|
const normalized = String(value ?? "").trim();
|
|
56
59
|
return normalized || "(detached)";
|
|
57
60
|
}
|
|
58
|
-
function
|
|
61
|
+
function isBoundBranchMatch(currentBranch, branchName) {
|
|
59
62
|
const current = String(currentBranch ?? "").trim();
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
62
|
-
return current ===
|
|
63
|
+
const expected = String(branchName ?? "").trim();
|
|
64
|
+
if (!expected || !current) return true;
|
|
65
|
+
return current === expected;
|
|
63
66
|
}
|
|
64
|
-
function
|
|
67
|
+
function buildBranchMismatchHint(params) {
|
|
65
68
|
const overrideFlag = params.overrideFlag?.trim() || "--allow-branch-mismatch";
|
|
66
69
|
return [
|
|
67
70
|
`Current branch: ${describeBranch(params.currentBranch)}`,
|
|
68
|
-
`
|
|
69
|
-
`Switch to ${describeBranch(params.
|
|
71
|
+
`Bound branch: ${describeBranch(params.branchName)}`,
|
|
72
|
+
`Switch to ${describeBranch(params.branchName)} or rerun with ${overrideFlag} if this is intentional.`
|
|
70
73
|
].join("\n");
|
|
71
74
|
}
|
|
72
|
-
function
|
|
75
|
+
function assertBoundBranchMatch(params) {
|
|
73
76
|
if (params.allowBranchMismatch) return;
|
|
74
|
-
if (
|
|
75
|
-
throw new RemixError(`Current branch does not match this checkout's Remix
|
|
77
|
+
if (isBoundBranchMatch(params.currentBranch, params.branchName)) return;
|
|
78
|
+
throw new RemixError(`Current branch does not match this checkout's bound Remix branch while running ${params.operation}.`, {
|
|
76
79
|
code: REMIX_ERROR_CODES.PREFERRED_BRANCH_MISMATCH,
|
|
77
80
|
exitCode: 2,
|
|
78
|
-
hint:
|
|
81
|
+
hint: buildBranchMismatchHint({
|
|
79
82
|
currentBranch: params.currentBranch,
|
|
80
|
-
|
|
83
|
+
branchName: params.branchName,
|
|
81
84
|
overrideFlag: params.overrideFlag
|
|
82
85
|
})
|
|
83
86
|
});
|
|
@@ -141,6 +144,52 @@ function sanitizeCheckoutDirName(value) {
|
|
|
141
144
|
function buildDashboardAppUrl(appId) {
|
|
142
145
|
return `https://dashboard.remix.one/apps/${encodeURIComponent(appId)}`;
|
|
143
146
|
}
|
|
147
|
+
async function resolveProjectLaneIfAuthoritative(api, params) {
|
|
148
|
+
const branchName = String(params.branchName ?? "").trim();
|
|
149
|
+
if (!branchName) return null;
|
|
150
|
+
if (!params.projectId && !params.repoFingerprint && !params.remoteUrl) return null;
|
|
151
|
+
const readLane = async () => {
|
|
152
|
+
const laneResp = await api.resolveProjectLaneBinding({
|
|
153
|
+
projectId: params.projectId ?? void 0,
|
|
154
|
+
repoFingerprint: params.repoFingerprint ?? void 0,
|
|
155
|
+
remoteUrl: params.remoteUrl ?? void 0,
|
|
156
|
+
defaultBranch: params.defaultBranch ?? void 0,
|
|
157
|
+
branchName
|
|
158
|
+
});
|
|
159
|
+
return unwrapResponseObject(laneResp, "project lane binding");
|
|
160
|
+
};
|
|
161
|
+
let lane = await readLane();
|
|
162
|
+
const shouldRepairResolvedLane = lane.status === "resolved" && Boolean(
|
|
163
|
+
params.expectedUpstreamAppId && (!lane.upstreamAppId || lane.upstreamAppId !== params.expectedUpstreamAppId)
|
|
164
|
+
);
|
|
165
|
+
if (shouldRepairResolvedLane && params.createIfMissing && params.seedAppId) {
|
|
166
|
+
const ensuredResp = await api.ensureProjectLaneBinding({
|
|
167
|
+
projectId: params.projectId ?? void 0,
|
|
168
|
+
repoFingerprint: params.repoFingerprint ?? void 0,
|
|
169
|
+
remoteUrl: params.remoteUrl ?? void 0,
|
|
170
|
+
defaultBranch: params.defaultBranch ?? void 0,
|
|
171
|
+
branchName,
|
|
172
|
+
seedAppId: params.seedAppId
|
|
173
|
+
});
|
|
174
|
+
lane = unwrapResponseObject(ensuredResp, "project lane binding");
|
|
175
|
+
}
|
|
176
|
+
if (lane.status !== "resolved" && params.createIfMissing && params.seedAppId) {
|
|
177
|
+
const ensuredResp = await api.ensureProjectLaneBinding({
|
|
178
|
+
projectId: params.projectId ?? void 0,
|
|
179
|
+
repoFingerprint: params.repoFingerprint ?? void 0,
|
|
180
|
+
remoteUrl: params.remoteUrl ?? void 0,
|
|
181
|
+
defaultBranch: params.defaultBranch ?? void 0,
|
|
182
|
+
branchName,
|
|
183
|
+
seedAppId: params.seedAppId
|
|
184
|
+
});
|
|
185
|
+
lane = unwrapResponseObject(ensuredResp, "project lane binding");
|
|
186
|
+
}
|
|
187
|
+
if (lane.status !== "resolved") return null;
|
|
188
|
+
if (params.projectId && lane.projectId && lane.projectId !== params.projectId) return null;
|
|
189
|
+
if (params.currentAppId && lane.currentAppId && lane.currentAppId !== params.currentAppId) return null;
|
|
190
|
+
if (params.expectedUpstreamAppId && lane.upstreamAppId && lane.upstreamAppId !== params.expectedUpstreamAppId) return null;
|
|
191
|
+
return lane;
|
|
192
|
+
}
|
|
144
193
|
async function pollAppReady(api, appId) {
|
|
145
194
|
const started = Date.now();
|
|
146
195
|
let delay = 2e3;
|
|
@@ -285,6 +334,239 @@ async function pollMergeRequestCompletion(api, mrId, params) {
|
|
|
285
334
|
throw new RemixError("Timed out waiting for merge approval to complete.", { exitCode: 1 });
|
|
286
335
|
}
|
|
287
336
|
|
|
337
|
+
// src/application/collab/laneBindingResolver.ts
|
|
338
|
+
function normalizeBranchName(value) {
|
|
339
|
+
const normalized = String(value ?? "").trim();
|
|
340
|
+
return normalized || null;
|
|
341
|
+
}
|
|
342
|
+
function buildBindingFromLane(state, lane) {
|
|
343
|
+
if (!lane.currentAppId || !lane.upstreamAppId) return null;
|
|
344
|
+
const resolvedBranch = normalizeBranchName(lane.branchName) ?? state.currentBranch ?? null;
|
|
345
|
+
const resolvedDefaultBranch = normalizeBranchName(lane.defaultBranch) ?? normalizeBranchName(state.defaultBranch);
|
|
346
|
+
const explicitRootProjectId = state.explicitRootBinding?.projectId ?? null;
|
|
347
|
+
const bindingMode = explicitRootProjectId && lane.projectId === explicitRootProjectId && resolvedBranch && resolvedBranch === resolvedDefaultBranch ? "explicit_root" : "lane";
|
|
348
|
+
return {
|
|
349
|
+
schemaVersion: 3,
|
|
350
|
+
projectId: lane.projectId ?? state.projectId,
|
|
351
|
+
currentAppId: lane.currentAppId,
|
|
352
|
+
upstreamAppId: lane.upstreamAppId,
|
|
353
|
+
threadId: lane.threadId ?? null,
|
|
354
|
+
repoFingerprint: lane.repoFingerprint ?? state.repoFingerprint ?? null,
|
|
355
|
+
remoteUrl: lane.remoteUrl ?? state.remoteUrl ?? null,
|
|
356
|
+
defaultBranch: lane.defaultBranch ?? state.defaultBranch ?? null,
|
|
357
|
+
laneId: lane.laneId ?? null,
|
|
358
|
+
branchName: resolvedBranch,
|
|
359
|
+
bindingMode
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function shouldPersistRemoteLaneMetadata(localBinding, lane) {
|
|
363
|
+
return Boolean(
|
|
364
|
+
!localBinding.laneId && lane.laneId || !localBinding.threadId && lane.threadId || !localBinding.repoFingerprint && lane.repoFingerprint || !localBinding.remoteUrl && lane.remoteUrl || !localBinding.defaultBranch && lane.defaultBranch || !localBinding.branchName && lane.branchName
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
function shouldRequireRemoteLaneForCurrentBranch(params) {
|
|
368
|
+
if (!params.currentBranch) return false;
|
|
369
|
+
const defaultBranch = normalizeBranchName(params.defaultBranch);
|
|
370
|
+
if (params.currentBranch === defaultBranch) return false;
|
|
371
|
+
return !params.binding.laneId || params.binding.currentAppId === params.binding.upstreamAppId;
|
|
372
|
+
}
|
|
373
|
+
async function persistResolvedLane(repoRoot, binding) {
|
|
374
|
+
await writeCollabBinding(repoRoot, {
|
|
375
|
+
projectId: binding.projectId,
|
|
376
|
+
currentAppId: binding.currentAppId,
|
|
377
|
+
upstreamAppId: binding.upstreamAppId,
|
|
378
|
+
threadId: binding.threadId,
|
|
379
|
+
repoFingerprint: binding.repoFingerprint,
|
|
380
|
+
remoteUrl: binding.remoteUrl,
|
|
381
|
+
defaultBranch: binding.defaultBranch,
|
|
382
|
+
laneId: binding.laneId,
|
|
383
|
+
branchName: binding.branchName,
|
|
384
|
+
bindingMode: binding.bindingMode
|
|
385
|
+
});
|
|
386
|
+
return readCollabBinding(repoRoot);
|
|
387
|
+
}
|
|
388
|
+
function buildAmbiguousResolution(params) {
|
|
389
|
+
return {
|
|
390
|
+
status: "ambiguous_family_selection",
|
|
391
|
+
currentBranch: params.currentBranch,
|
|
392
|
+
projectIds: Array.isArray(params.lane.projectIds) ? params.lane.projectIds.filter((value) => typeof value === "string" && value.trim().length > 0) : [],
|
|
393
|
+
repoFingerprint: params.lane.repoFingerprint ?? params.state.repoFingerprint,
|
|
394
|
+
remoteUrl: params.lane.remoteUrl ?? params.state.remoteUrl,
|
|
395
|
+
defaultBranch: params.lane.defaultBranch ?? params.state.defaultBranch
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
async function resolveActiveLaneBinding(params) {
|
|
399
|
+
const state = await readCollabBindingState(params.repoRoot);
|
|
400
|
+
if (!state) {
|
|
401
|
+
return { status: "not_bound", currentBranch: null };
|
|
402
|
+
}
|
|
403
|
+
const currentBranch = normalizeBranchName(state.currentBranch);
|
|
404
|
+
const localBinding = state.binding;
|
|
405
|
+
if (localBinding) {
|
|
406
|
+
const requireRemoteLane = shouldRequireRemoteLaneForCurrentBranch({
|
|
407
|
+
binding: localBinding,
|
|
408
|
+
currentBranch,
|
|
409
|
+
defaultBranch: state.defaultBranch
|
|
410
|
+
});
|
|
411
|
+
if (!params.api || !currentBranch) {
|
|
412
|
+
return {
|
|
413
|
+
status: "resolved",
|
|
414
|
+
source: "local",
|
|
415
|
+
binding: localBinding,
|
|
416
|
+
currentBranch
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
const laneResp2 = await params.api.resolveProjectLaneBinding({
|
|
420
|
+
projectId: state.explicitRootBinding?.projectId ?? (requireRemoteLane ? void 0 : localBinding.projectId ?? state.projectId ?? void 0),
|
|
421
|
+
repoFingerprint: state.repoFingerprint ?? void 0,
|
|
422
|
+
remoteUrl: state.remoteUrl ?? void 0,
|
|
423
|
+
defaultBranch: state.defaultBranch ?? void 0,
|
|
424
|
+
branchName: currentBranch
|
|
425
|
+
});
|
|
426
|
+
const lane2 = unwrapResponseObject(laneResp2, "project lane binding");
|
|
427
|
+
if (lane2.status === "ambiguous_family_selection") {
|
|
428
|
+
return buildAmbiguousResolution({ state, currentBranch, lane: lane2 });
|
|
429
|
+
}
|
|
430
|
+
if (lane2.status === "resolved") {
|
|
431
|
+
const resolvedBranch = normalizeBranchName(lane2.branchName);
|
|
432
|
+
const resolvedProjectId = lane2.projectId ?? state.projectId;
|
|
433
|
+
const branchConflict = Boolean(resolvedBranch && localBinding.branchName && resolvedBranch !== localBinding.branchName);
|
|
434
|
+
const appConflict = Boolean(lane2.currentAppId && lane2.currentAppId !== localBinding.currentAppId);
|
|
435
|
+
const upstreamConflict = Boolean(lane2.upstreamAppId && lane2.upstreamAppId !== localBinding.upstreamAppId);
|
|
436
|
+
const projectConflict = Boolean(resolvedProjectId && localBinding.projectId && resolvedProjectId !== localBinding.projectId);
|
|
437
|
+
if (branchConflict || appConflict || upstreamConflict || projectConflict) {
|
|
438
|
+
if (requireRemoteLane) {
|
|
439
|
+
const binding = buildBindingFromLane(state, lane2);
|
|
440
|
+
if (binding) {
|
|
441
|
+
return {
|
|
442
|
+
status: "resolved",
|
|
443
|
+
source: "remote",
|
|
444
|
+
binding,
|
|
445
|
+
currentBranch
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
status: "binding_conflict",
|
|
451
|
+
binding: localBinding,
|
|
452
|
+
resolvedLane: lane2,
|
|
453
|
+
currentBranch
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
if (shouldPersistRemoteLaneMetadata(localBinding, lane2)) {
|
|
457
|
+
const binding = buildBindingFromLane(state, lane2);
|
|
458
|
+
if (binding) {
|
|
459
|
+
return {
|
|
460
|
+
status: "resolved",
|
|
461
|
+
source: "remote",
|
|
462
|
+
binding,
|
|
463
|
+
currentBranch
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (requireRemoteLane) {
|
|
469
|
+
return {
|
|
470
|
+
status: "missing_branch_binding",
|
|
471
|
+
currentBranch,
|
|
472
|
+
projectId: lane2.projectId ?? state.projectId,
|
|
473
|
+
repoFingerprint: lane2.repoFingerprint ?? state.repoFingerprint,
|
|
474
|
+
remoteUrl: lane2.remoteUrl ?? state.remoteUrl,
|
|
475
|
+
defaultBranch: lane2.defaultBranch ?? state.defaultBranch,
|
|
476
|
+
upstreamAppId: lane2.upstreamAppId ?? localBinding.upstreamAppId ?? null,
|
|
477
|
+
threadId: lane2.threadId ?? localBinding.threadId ?? null
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
status: "resolved",
|
|
482
|
+
source: "local",
|
|
483
|
+
binding: localBinding,
|
|
484
|
+
currentBranch
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
if (!params.api || !currentBranch) {
|
|
488
|
+
return {
|
|
489
|
+
status: "missing_branch_binding",
|
|
490
|
+
currentBranch,
|
|
491
|
+
projectId: state.projectId,
|
|
492
|
+
repoFingerprint: state.repoFingerprint,
|
|
493
|
+
remoteUrl: state.remoteUrl,
|
|
494
|
+
defaultBranch: state.defaultBranch,
|
|
495
|
+
upstreamAppId: null,
|
|
496
|
+
threadId: null
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
const laneResp = await params.api.resolveProjectLaneBinding({
|
|
500
|
+
projectId: state.explicitRootBinding?.projectId ?? state.projectId ?? void 0,
|
|
501
|
+
repoFingerprint: state.repoFingerprint ?? void 0,
|
|
502
|
+
remoteUrl: state.remoteUrl ?? void 0,
|
|
503
|
+
defaultBranch: state.defaultBranch ?? void 0,
|
|
504
|
+
branchName: currentBranch
|
|
505
|
+
});
|
|
506
|
+
const lane = unwrapResponseObject(laneResp, "project lane binding");
|
|
507
|
+
if (lane.status === "ambiguous_family_selection") {
|
|
508
|
+
return buildAmbiguousResolution({ state, currentBranch, lane });
|
|
509
|
+
}
|
|
510
|
+
if (lane.status === "resolved") {
|
|
511
|
+
const binding = buildBindingFromLane(state, lane);
|
|
512
|
+
if (binding) {
|
|
513
|
+
return {
|
|
514
|
+
status: "resolved",
|
|
515
|
+
source: "remote",
|
|
516
|
+
binding,
|
|
517
|
+
currentBranch
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
status: "missing_branch_binding",
|
|
523
|
+
currentBranch,
|
|
524
|
+
projectId: lane.projectId ?? state.explicitRootBinding?.projectId ?? state.projectId,
|
|
525
|
+
repoFingerprint: lane.repoFingerprint ?? state.repoFingerprint,
|
|
526
|
+
remoteUrl: lane.remoteUrl ?? state.remoteUrl,
|
|
527
|
+
defaultBranch: lane.defaultBranch ?? state.defaultBranch,
|
|
528
|
+
upstreamAppId: lane.upstreamAppId ?? state.explicitRootBinding?.upstreamAppId ?? null,
|
|
529
|
+
threadId: lane.threadId ?? state.explicitRootBinding?.threadId ?? null
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
async function ensureActiveLaneBinding(params) {
|
|
533
|
+
const resolved = await resolveActiveLaneBinding({
|
|
534
|
+
repoRoot: params.repoRoot,
|
|
535
|
+
api: params.api
|
|
536
|
+
});
|
|
537
|
+
if (resolved.status === "resolved") {
|
|
538
|
+
if (resolved.source === "local") {
|
|
539
|
+
return resolved.binding;
|
|
540
|
+
}
|
|
541
|
+
return persistResolvedLane(params.repoRoot, resolved.binding);
|
|
542
|
+
}
|
|
543
|
+
if (resolved.status === "binding_conflict") {
|
|
544
|
+
throw new RemixError("Current branch binding conflicts with the server-resolved Remix lane.", {
|
|
545
|
+
exitCode: 2,
|
|
546
|
+
hint: `Local app ${resolved.binding.currentAppId}; server app ${resolved.resolvedLane.currentAppId ?? "(unknown)"}. Repair the branch binding before running ${params.operation ?? "this command"}.`
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
if (resolved.status === "ambiguous_family_selection") {
|
|
550
|
+
throw new RemixError("Multiple canonical Remix families match this repository.", {
|
|
551
|
+
exitCode: 2,
|
|
552
|
+
hint: "This checkout is not specific enough to choose a single family for the current branch. Continue from a checkout already bound to the intended family, or run `remix collab init --force-new` to create a new canonical family."
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
if (resolved.status === "not_bound") {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
if (!resolved.currentBranch) {
|
|
559
|
+
throw new RemixError("Current branch is not yet bound to a Remix lane.", {
|
|
560
|
+
exitCode: 2,
|
|
561
|
+
hint: `Switch to a named branch before running ${params.operation ?? "this command"}.`
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
throw new RemixError("Current branch is not yet bound to a Remix lane.", {
|
|
565
|
+
exitCode: 2,
|
|
566
|
+
hint: `Run \`remix collab init\` on branch ${resolved.currentBranch} before running ${params.operation ?? "this command"}.`
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
288
570
|
// src/application/collab/recordingPreflight.ts
|
|
289
571
|
async function collabRecordingPreflight(params) {
|
|
290
572
|
let repoRoot;
|
|
@@ -297,7 +579,7 @@ async function collabRecordingPreflight(params) {
|
|
|
297
579
|
repoRoot: null,
|
|
298
580
|
appId: null,
|
|
299
581
|
currentBranch: null,
|
|
300
|
-
|
|
582
|
+
branchName: null,
|
|
301
583
|
headCommitHash: null,
|
|
302
584
|
worktreeClean: false,
|
|
303
585
|
syncStatus: null,
|
|
@@ -309,14 +591,14 @@ async function collabRecordingPreflight(params) {
|
|
|
309
591
|
hint: message
|
|
310
592
|
};
|
|
311
593
|
}
|
|
312
|
-
const
|
|
313
|
-
if (
|
|
594
|
+
const bindingResolution = await resolveActiveLaneBinding({ repoRoot, api: params.api });
|
|
595
|
+
if (bindingResolution.status === "not_bound") {
|
|
314
596
|
return {
|
|
315
597
|
status: "not_bound",
|
|
316
598
|
repoRoot,
|
|
317
599
|
appId: null,
|
|
318
600
|
currentBranch: null,
|
|
319
|
-
|
|
601
|
+
branchName: null,
|
|
320
602
|
headCommitHash: null,
|
|
321
603
|
worktreeClean: false,
|
|
322
604
|
syncStatus: null,
|
|
@@ -328,19 +610,74 @@ async function collabRecordingPreflight(params) {
|
|
|
328
610
|
hint: "Run `remix collab init` first."
|
|
329
611
|
};
|
|
330
612
|
}
|
|
613
|
+
if (bindingResolution.status === "missing_branch_binding") {
|
|
614
|
+
return {
|
|
615
|
+
status: "branch_binding_missing",
|
|
616
|
+
repoRoot,
|
|
617
|
+
appId: null,
|
|
618
|
+
currentBranch: bindingResolution.currentBranch,
|
|
619
|
+
branchName: bindingResolution.currentBranch,
|
|
620
|
+
headCommitHash: null,
|
|
621
|
+
worktreeClean: false,
|
|
622
|
+
syncStatus: null,
|
|
623
|
+
syncTargetCommitHash: null,
|
|
624
|
+
syncTargetCommitId: null,
|
|
625
|
+
reconcileTargetHeadCommitHash: null,
|
|
626
|
+
reconcileTargetHeadCommitId: null,
|
|
627
|
+
warnings: [],
|
|
628
|
+
hint: `Current branch ${bindingResolution.currentBranch ?? "(detached)"} is not yet bound to a Remix lane.`
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
if (bindingResolution.status === "ambiguous_family_selection") {
|
|
632
|
+
return {
|
|
633
|
+
status: "family_ambiguous",
|
|
634
|
+
repoRoot,
|
|
635
|
+
appId: null,
|
|
636
|
+
currentBranch: bindingResolution.currentBranch,
|
|
637
|
+
branchName: bindingResolution.currentBranch,
|
|
638
|
+
headCommitHash: null,
|
|
639
|
+
worktreeClean: false,
|
|
640
|
+
syncStatus: null,
|
|
641
|
+
syncTargetCommitHash: null,
|
|
642
|
+
syncTargetCommitId: null,
|
|
643
|
+
reconcileTargetHeadCommitHash: null,
|
|
644
|
+
reconcileTargetHeadCommitId: null,
|
|
645
|
+
warnings: [],
|
|
646
|
+
hint: "Multiple canonical Remix families match this repository. Continue from a checkout already bound to the intended family, or run `remix collab init --force-new` to create a new canonical family."
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
if (bindingResolution.status === "binding_conflict") {
|
|
650
|
+
return {
|
|
651
|
+
status: "metadata_conflict",
|
|
652
|
+
repoRoot,
|
|
653
|
+
appId: bindingResolution.binding.currentAppId,
|
|
654
|
+
currentBranch: bindingResolution.currentBranch,
|
|
655
|
+
branchName: bindingResolution.binding.branchName,
|
|
656
|
+
headCommitHash: null,
|
|
657
|
+
worktreeClean: false,
|
|
658
|
+
syncStatus: null,
|
|
659
|
+
syncTargetCommitHash: null,
|
|
660
|
+
syncTargetCommitId: null,
|
|
661
|
+
reconcileTargetHeadCommitHash: null,
|
|
662
|
+
reconcileTargetHeadCommitId: null,
|
|
663
|
+
warnings: [],
|
|
664
|
+
hint: `Local binding for ${bindingResolution.currentBranch ?? "(detached)"} points to app ${bindingResolution.binding.currentAppId}, but the server resolved lane ${bindingResolution.resolvedLane.laneId ?? "(unknown)"} / app ${bindingResolution.resolvedLane.currentAppId ?? "(unknown)"}. Repair the branch binding before recording work.`
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
const binding = bindingResolution.binding;
|
|
331
668
|
const [currentBranch, headCommitHash, worktreeStatus] = await Promise.all([
|
|
332
669
|
getCurrentBranch(repoRoot),
|
|
333
670
|
getHeadCommitHash(repoRoot),
|
|
334
671
|
getWorktreeStatus(repoRoot)
|
|
335
672
|
]);
|
|
336
|
-
const
|
|
673
|
+
const branchName = binding.branchName ?? null;
|
|
337
674
|
if (!headCommitHash) {
|
|
338
675
|
return {
|
|
339
676
|
status: "missing_head",
|
|
340
677
|
repoRoot,
|
|
341
678
|
appId: binding.currentAppId,
|
|
342
679
|
currentBranch,
|
|
343
|
-
|
|
680
|
+
branchName,
|
|
344
681
|
headCommitHash: null,
|
|
345
682
|
worktreeClean: worktreeStatus.isClean,
|
|
346
683
|
syncStatus: null,
|
|
@@ -352,13 +689,13 @@ async function collabRecordingPreflight(params) {
|
|
|
352
689
|
hint: "Failed to resolve local HEAD commit."
|
|
353
690
|
};
|
|
354
691
|
}
|
|
355
|
-
if (!params.allowBranchMismatch && !
|
|
692
|
+
if (!params.allowBranchMismatch && !isBoundBranchMatch(currentBranch, branchName)) {
|
|
356
693
|
return {
|
|
357
694
|
status: "branch_mismatch",
|
|
358
695
|
repoRoot,
|
|
359
696
|
appId: binding.currentAppId,
|
|
360
697
|
currentBranch,
|
|
361
|
-
|
|
698
|
+
branchName,
|
|
362
699
|
headCommitHash,
|
|
363
700
|
worktreeClean: worktreeStatus.isClean,
|
|
364
701
|
syncStatus: null,
|
|
@@ -367,9 +704,9 @@ async function collabRecordingPreflight(params) {
|
|
|
367
704
|
reconcileTargetHeadCommitHash: null,
|
|
368
705
|
reconcileTargetHeadCommitId: null,
|
|
369
706
|
warnings: [],
|
|
370
|
-
hint:
|
|
707
|
+
hint: buildBranchMismatchHint({
|
|
371
708
|
currentBranch,
|
|
372
|
-
|
|
709
|
+
branchName
|
|
373
710
|
})
|
|
374
711
|
};
|
|
375
712
|
}
|
|
@@ -387,7 +724,7 @@ async function collabRecordingPreflight(params) {
|
|
|
387
724
|
repoRoot,
|
|
388
725
|
appId: binding.currentAppId,
|
|
389
726
|
currentBranch,
|
|
390
|
-
|
|
727
|
+
branchName,
|
|
391
728
|
headCommitHash,
|
|
392
729
|
worktreeClean: worktreeStatus.isClean,
|
|
393
730
|
syncStatus: sync.status,
|
|
@@ -405,7 +742,7 @@ async function collabRecordingPreflight(params) {
|
|
|
405
742
|
repoRoot,
|
|
406
743
|
appId: binding.currentAppId,
|
|
407
744
|
currentBranch,
|
|
408
|
-
|
|
745
|
+
branchName,
|
|
409
746
|
headCommitHash,
|
|
410
747
|
worktreeClean: worktreeStatus.isClean,
|
|
411
748
|
syncStatus: sync.status,
|
|
@@ -430,7 +767,7 @@ async function collabRecordingPreflight(params) {
|
|
|
430
767
|
repoRoot,
|
|
431
768
|
appId: binding.currentAppId,
|
|
432
769
|
currentBranch,
|
|
433
|
-
|
|
770
|
+
branchName,
|
|
434
771
|
headCommitHash,
|
|
435
772
|
worktreeClean: worktreeStatus.isClean,
|
|
436
773
|
syncStatus: sync.status,
|
|
@@ -448,7 +785,7 @@ async function collabRecordingPreflight(params) {
|
|
|
448
785
|
repoRoot,
|
|
449
786
|
appId: binding.currentAppId,
|
|
450
787
|
currentBranch,
|
|
451
|
-
|
|
788
|
+
branchName,
|
|
452
789
|
headCommitHash,
|
|
453
790
|
worktreeClean: worktreeStatus.isClean,
|
|
454
791
|
syncStatus: sync.status,
|
|
@@ -465,7 +802,7 @@ async function collabRecordingPreflight(params) {
|
|
|
465
802
|
repoRoot,
|
|
466
803
|
appId: binding.currentAppId,
|
|
467
804
|
currentBranch,
|
|
468
|
-
|
|
805
|
+
branchName,
|
|
469
806
|
headCommitHash,
|
|
470
807
|
worktreeClean: worktreeStatus.isClean,
|
|
471
808
|
syncStatus: sync.status,
|
|
@@ -697,7 +1034,11 @@ import os2 from "os";
|
|
|
697
1034
|
import path2 from "path";
|
|
698
1035
|
async function collabSync(params) {
|
|
699
1036
|
const repoRoot = await findGitRoot(params.cwd);
|
|
700
|
-
const binding = await
|
|
1037
|
+
const binding = await ensureActiveLaneBinding({
|
|
1038
|
+
repoRoot,
|
|
1039
|
+
api: params.api,
|
|
1040
|
+
operation: "`remix collab sync`"
|
|
1041
|
+
});
|
|
701
1042
|
if (!binding) {
|
|
702
1043
|
throw new RemixError("Repository is not bound to Remix.", {
|
|
703
1044
|
exitCode: 2,
|
|
@@ -706,9 +1047,9 @@ async function collabSync(params) {
|
|
|
706
1047
|
}
|
|
707
1048
|
await ensureCleanWorktree(repoRoot);
|
|
708
1049
|
const branch = await requireCurrentBranch(repoRoot);
|
|
709
|
-
|
|
1050
|
+
assertBoundBranchMatch({
|
|
710
1051
|
currentBranch: branch,
|
|
711
|
-
|
|
1052
|
+
branchName: binding.branchName,
|
|
712
1053
|
allowBranchMismatch: params.allowBranchMismatch,
|
|
713
1054
|
operation: "`remix collab sync`"
|
|
714
1055
|
});
|
|
@@ -785,9 +1126,9 @@ async function collabSync(params) {
|
|
|
785
1126
|
});
|
|
786
1127
|
await ensureCleanWorktree(lockedRepoRoot);
|
|
787
1128
|
const lockedBranch = await requireCurrentBranch(lockedRepoRoot);
|
|
788
|
-
|
|
1129
|
+
assertBoundBranchMatch({
|
|
789
1130
|
currentBranch: lockedBranch,
|
|
790
|
-
|
|
1131
|
+
branchName: binding.branchName,
|
|
791
1132
|
allowBranchMismatch: params.allowBranchMismatch,
|
|
792
1133
|
operation: "`remix collab sync`"
|
|
793
1134
|
});
|
|
@@ -820,6 +1161,18 @@ function assertSupportedRecordingPreflight(preflight) {
|
|
|
820
1161
|
hint: preflight.hint
|
|
821
1162
|
});
|
|
822
1163
|
}
|
|
1164
|
+
if (preflight.status === "branch_binding_missing") {
|
|
1165
|
+
throw new RemixError("Current branch is not yet bound to a Remix lane.", {
|
|
1166
|
+
exitCode: 2,
|
|
1167
|
+
hint: preflight.hint
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
if (preflight.status === "family_ambiguous") {
|
|
1171
|
+
throw new RemixError("Multiple canonical Remix families match this repository.", {
|
|
1172
|
+
exitCode: 2,
|
|
1173
|
+
hint: preflight.hint
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
823
1176
|
if (preflight.status === "not_git_repo") {
|
|
824
1177
|
throw new RemixError(preflight.hint || "Not inside a git repository.", {
|
|
825
1178
|
exitCode: 2,
|
|
@@ -833,9 +1186,9 @@ function assertSupportedRecordingPreflight(preflight) {
|
|
|
833
1186
|
});
|
|
834
1187
|
}
|
|
835
1188
|
if (preflight.status === "branch_mismatch") {
|
|
836
|
-
|
|
1189
|
+
assertBoundBranchMatch({
|
|
837
1190
|
currentBranch: preflight.currentBranch,
|
|
838
|
-
|
|
1191
|
+
branchName: preflight.branchName,
|
|
839
1192
|
allowBranchMismatch: false,
|
|
840
1193
|
operation: "`remix collab add`"
|
|
841
1194
|
});
|
|
@@ -855,7 +1208,11 @@ function assertSupportedRecordingPreflight(preflight) {
|
|
|
855
1208
|
}
|
|
856
1209
|
async function collabAdd(params) {
|
|
857
1210
|
const repoRoot = await findGitRoot(params.cwd);
|
|
858
|
-
const binding = await
|
|
1211
|
+
const binding = await ensureActiveLaneBinding({
|
|
1212
|
+
repoRoot,
|
|
1213
|
+
api: params.api,
|
|
1214
|
+
operation: "`remix collab add`"
|
|
1215
|
+
});
|
|
859
1216
|
if (!binding) {
|
|
860
1217
|
throw new RemixError("Repository is not bound to Remix.", {
|
|
861
1218
|
exitCode: 2,
|
|
@@ -876,9 +1233,9 @@ async function collabAdd(params) {
|
|
|
876
1233
|
});
|
|
877
1234
|
assertSupportedRecordingPreflight(preflight);
|
|
878
1235
|
const branch = preflight.currentBranch;
|
|
879
|
-
|
|
1236
|
+
assertBoundBranchMatch({
|
|
880
1237
|
currentBranch: branch,
|
|
881
|
-
|
|
1238
|
+
branchName: binding.branchName,
|
|
882
1239
|
allowBranchMismatch: params.allowBranchMismatch,
|
|
883
1240
|
operation: "`remix collab add`"
|
|
884
1241
|
});
|
|
@@ -1051,6 +1408,7 @@ async function collabAdd(params) {
|
|
|
1051
1408
|
});
|
|
1052
1409
|
const resp = await params.api.createChangeStep(binding.currentAppId, {
|
|
1053
1410
|
threadId: binding.threadId ?? void 0,
|
|
1411
|
+
collabLaneId: binding.laneId ?? void 0,
|
|
1054
1412
|
prompt,
|
|
1055
1413
|
assistantResponse: assistantResponse ?? void 0,
|
|
1056
1414
|
diff,
|
|
@@ -1126,6 +1484,18 @@ function assertSupportedRecordingPreflight2(preflight) {
|
|
|
1126
1484
|
hint: preflight.hint
|
|
1127
1485
|
});
|
|
1128
1486
|
}
|
|
1487
|
+
if (preflight.status === "branch_binding_missing") {
|
|
1488
|
+
throw new RemixError("Current branch is not yet bound to a Remix lane.", {
|
|
1489
|
+
exitCode: 2,
|
|
1490
|
+
hint: preflight.hint
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
if (preflight.status === "family_ambiguous") {
|
|
1494
|
+
throw new RemixError("Multiple canonical Remix families match this repository.", {
|
|
1495
|
+
exitCode: 2,
|
|
1496
|
+
hint: preflight.hint
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1129
1499
|
if (preflight.status === "not_git_repo") {
|
|
1130
1500
|
throw new RemixError(preflight.hint || "Not inside a git repository.", {
|
|
1131
1501
|
exitCode: 2,
|
|
@@ -1139,9 +1509,9 @@ function assertSupportedRecordingPreflight2(preflight) {
|
|
|
1139
1509
|
});
|
|
1140
1510
|
}
|
|
1141
1511
|
if (preflight.status === "branch_mismatch") {
|
|
1142
|
-
|
|
1512
|
+
assertBoundBranchMatch({
|
|
1143
1513
|
currentBranch: preflight.currentBranch,
|
|
1144
|
-
|
|
1514
|
+
branchName: preflight.branchName,
|
|
1145
1515
|
allowBranchMismatch: false,
|
|
1146
1516
|
operation: "`remix collab record-turn`"
|
|
1147
1517
|
});
|
|
@@ -1161,7 +1531,11 @@ function assertSupportedRecordingPreflight2(preflight) {
|
|
|
1161
1531
|
}
|
|
1162
1532
|
async function collabRecordTurn(params) {
|
|
1163
1533
|
const repoRoot = await findGitRoot(params.cwd);
|
|
1164
|
-
const binding = await
|
|
1534
|
+
const binding = await ensureActiveLaneBinding({
|
|
1535
|
+
repoRoot,
|
|
1536
|
+
api: params.api,
|
|
1537
|
+
operation: "`remix collab record-turn`"
|
|
1538
|
+
});
|
|
1165
1539
|
if (!binding) {
|
|
1166
1540
|
throw new RemixError("Repository is not bound to Remix.", {
|
|
1167
1541
|
exitCode: 2,
|
|
@@ -1193,9 +1567,9 @@ async function collabRecordTurn(params) {
|
|
|
1193
1567
|
});
|
|
1194
1568
|
}
|
|
1195
1569
|
const branch = await getCurrentBranch(repoRoot);
|
|
1196
|
-
|
|
1570
|
+
assertBoundBranchMatch({
|
|
1197
1571
|
currentBranch: branch,
|
|
1198
|
-
|
|
1572
|
+
branchName: binding.branchName,
|
|
1199
1573
|
allowBranchMismatch: params.allowBranchMismatch,
|
|
1200
1574
|
operation: "`remix collab record-turn`"
|
|
1201
1575
|
});
|
|
@@ -1209,6 +1583,7 @@ async function collabRecordTurn(params) {
|
|
|
1209
1583
|
});
|
|
1210
1584
|
const resp = await params.api.createCollabTurn(binding.currentAppId, {
|
|
1211
1585
|
threadId: binding.threadId ?? void 0,
|
|
1586
|
+
collabLaneId: binding.laneId ?? void 0,
|
|
1212
1587
|
prompt,
|
|
1213
1588
|
assistantResponse,
|
|
1214
1589
|
actor: params.actor,
|
|
@@ -1221,7 +1596,8 @@ async function collabRecordTurn(params) {
|
|
|
1221
1596
|
},
|
|
1222
1597
|
idempotencyKey
|
|
1223
1598
|
});
|
|
1224
|
-
|
|
1599
|
+
const turn = unwrapResponseObject(resp, "collab turn");
|
|
1600
|
+
return turn;
|
|
1225
1601
|
}
|
|
1226
1602
|
|
|
1227
1603
|
// src/application/collab/collabFinalizeTurn.ts
|
|
@@ -1231,7 +1607,11 @@ function collectWarnings(value) {
|
|
|
1231
1607
|
}
|
|
1232
1608
|
async function collabFinalizeTurn(params) {
|
|
1233
1609
|
const repoRoot = await findGitRoot(params.cwd);
|
|
1234
|
-
const binding = await
|
|
1610
|
+
const binding = await ensureActiveLaneBinding({
|
|
1611
|
+
repoRoot,
|
|
1612
|
+
api: params.api,
|
|
1613
|
+
operation: "`remix collab finalize-turn`"
|
|
1614
|
+
});
|
|
1235
1615
|
if (!binding) {
|
|
1236
1616
|
throw new RemixError("Repository is not bound to Remix.", {
|
|
1237
1617
|
exitCode: 2,
|
|
@@ -1339,7 +1719,11 @@ async function collabApprove(params) {
|
|
|
1339
1719
|
operation: "collabApproveSyncTarget"
|
|
1340
1720
|
},
|
|
1341
1721
|
async ({ repoRoot, warnings }) => {
|
|
1342
|
-
const binding = await
|
|
1722
|
+
const binding = await ensureActiveLaneBinding({
|
|
1723
|
+
repoRoot,
|
|
1724
|
+
api: params.api,
|
|
1725
|
+
operation: "`remix collab approve --sync-target-repo`"
|
|
1726
|
+
});
|
|
1343
1727
|
if (!binding) {
|
|
1344
1728
|
throw new RemixError("Repository is not bound to Remix.", {
|
|
1345
1729
|
exitCode: 2,
|
|
@@ -1348,9 +1732,9 @@ async function collabApprove(params) {
|
|
|
1348
1732
|
}
|
|
1349
1733
|
await ensureCleanWorktree(repoRoot, "`remix collab approve --sync-target-repo`");
|
|
1350
1734
|
const branch = await requireCurrentBranch(repoRoot);
|
|
1351
|
-
|
|
1735
|
+
assertBoundBranchMatch({
|
|
1352
1736
|
currentBranch: branch,
|
|
1353
|
-
|
|
1737
|
+
branchName: binding.branchName,
|
|
1354
1738
|
allowBranchMismatch: params.allowBranchMismatch,
|
|
1355
1739
|
operation: "`remix collab approve --sync-target-repo`"
|
|
1356
1740
|
});
|
|
@@ -1433,10 +1817,6 @@ function isSubpath(parentPath, candidatePath) {
|
|
|
1433
1817
|
const relative = path4.relative(parentPath, candidatePath);
|
|
1434
1818
|
return relative === "" || !relative.startsWith("..") && !path4.isAbsolute(relative);
|
|
1435
1819
|
}
|
|
1436
|
-
function buildPreferredCheckoutBranch(appId) {
|
|
1437
|
-
const normalized = appId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1438
|
-
return `remix/remix/${normalized || "app"}`;
|
|
1439
|
-
}
|
|
1440
1820
|
async function resolveCheckoutDestination(params) {
|
|
1441
1821
|
if (params.outputDir?.trim()) {
|
|
1442
1822
|
const preferredRepoRoot = path4.resolve(params.outputDir.trim());
|
|
@@ -1501,7 +1881,9 @@ async function materializeAppCheckout(params) {
|
|
|
1501
1881
|
const bundle = await params.api.downloadAppBundle(params.appId);
|
|
1502
1882
|
await fs4.writeFile(bundlePath, bundle.data);
|
|
1503
1883
|
await cloneGitBundleToDirectory(bundlePath, repoRoot);
|
|
1504
|
-
|
|
1884
|
+
if (params.expectedBranchName?.trim()) {
|
|
1885
|
+
await checkoutLocalBranch(repoRoot, params.expectedBranchName.trim());
|
|
1886
|
+
}
|
|
1505
1887
|
await ensureGitInfoExcludeEntries(repoRoot, [".remix/"]);
|
|
1506
1888
|
} catch (err) {
|
|
1507
1889
|
await fs4.rm(repoRoot, { recursive: true, force: true }).catch(() => {
|
|
@@ -1510,15 +1892,15 @@ async function materializeAppCheckout(params) {
|
|
|
1510
1892
|
} finally {
|
|
1511
1893
|
await fs4.rm(bundleTempDir, { recursive: true, force: true });
|
|
1512
1894
|
}
|
|
1513
|
-
const
|
|
1514
|
-
const
|
|
1515
|
-
const
|
|
1895
|
+
const branchName = await getCurrentBranch(repoRoot) ?? params.expectedBranchName?.trim() ?? null;
|
|
1896
|
+
const remoteUrl = normalizeGitRemote(params.expectedRemoteUrl ?? await getRemoteOriginUrl(repoRoot));
|
|
1897
|
+
const defaultBranch = params.expectedDefaultBranch?.trim() ?? await getDefaultBranch(repoRoot) ?? branchName ?? null;
|
|
1516
1898
|
const repoFingerprint = remoteUrl ? await buildRepoFingerprint({ gitRoot: repoRoot, remoteUrl, defaultBranch }) : null;
|
|
1517
1899
|
return {
|
|
1518
1900
|
repoRoot,
|
|
1519
1901
|
remoteUrl,
|
|
1520
1902
|
defaultBranch,
|
|
1521
|
-
|
|
1903
|
+
branchName,
|
|
1522
1904
|
repoFingerprint
|
|
1523
1905
|
};
|
|
1524
1906
|
}
|
|
@@ -1533,29 +1915,55 @@ async function collabCheckout(params) {
|
|
|
1533
1915
|
});
|
|
1534
1916
|
}
|
|
1535
1917
|
const app = await pollAppReady(params.api, appId);
|
|
1918
|
+
const collab = app.collab && typeof app.collab === "object" ? app.collab : null;
|
|
1919
|
+
const source = app.source && typeof app.source === "object" ? app.source : null;
|
|
1920
|
+
const authoritativeBranchName = typeof collab?.branchName === "string" && collab.branchName.trim() || typeof source?.branch === "string" && source.branch.trim() || typeof source?.defaultBranch === "string" && source.defaultBranch.trim() || null;
|
|
1921
|
+
const authoritativeRemoteUrl = typeof source?.remoteUrl === "string" && source.remoteUrl.trim() || null;
|
|
1922
|
+
const authoritativeDefaultBranch = typeof source?.defaultBranch === "string" && source.defaultBranch.trim() || null;
|
|
1536
1923
|
const checkout = await materializeAppCheckout({
|
|
1537
1924
|
api: params.api,
|
|
1538
1925
|
cwd: params.cwd,
|
|
1539
1926
|
appId: String(app.id),
|
|
1540
1927
|
outputDir: params.outputDir ?? null,
|
|
1541
|
-
defaultDirName: sanitizeCheckoutDirName(String(app.name || app.id))
|
|
1928
|
+
defaultDirName: sanitizeCheckoutDirName(String(app.name || app.id)),
|
|
1929
|
+
expectedBranchName: authoritativeBranchName,
|
|
1930
|
+
expectedRemoteUrl: authoritativeRemoteUrl,
|
|
1931
|
+
expectedDefaultBranch: authoritativeDefaultBranch
|
|
1542
1932
|
});
|
|
1543
1933
|
const upstreamAppId = String(app.forkedFromAppId ?? app.id);
|
|
1544
|
-
const
|
|
1934
|
+
const laneId = typeof collab?.laneId === "string" ? collab.laneId : null;
|
|
1935
|
+
const repoFingerprint = typeof source?.repoFingerprint === "string" ? source.repoFingerprint : checkout.repoFingerprint;
|
|
1936
|
+
const remoteUrl = typeof source?.remoteUrl === "string" ? source.remoteUrl : checkout.remoteUrl;
|
|
1937
|
+
const defaultBranch = typeof source?.defaultBranch === "string" ? source.defaultBranch : checkout.defaultBranch;
|
|
1938
|
+
const branchName = authoritativeBranchName ?? checkout.branchName;
|
|
1939
|
+
const authoritativeLane = await resolveProjectLaneIfAuthoritative(params.api, {
|
|
1545
1940
|
projectId: String(app.projectId),
|
|
1941
|
+
repoFingerprint,
|
|
1942
|
+
remoteUrl,
|
|
1943
|
+
defaultBranch,
|
|
1944
|
+
branchName,
|
|
1546
1945
|
currentAppId: String(app.id),
|
|
1547
|
-
upstreamAppId,
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1946
|
+
expectedUpstreamAppId: upstreamAppId,
|
|
1947
|
+
createIfMissing: true,
|
|
1948
|
+
seedAppId: String(app.id)
|
|
1949
|
+
});
|
|
1950
|
+
const bindingPath = await writeCollabBinding(checkout.repoRoot, {
|
|
1951
|
+
projectId: authoritativeLane?.projectId ?? String(app.projectId),
|
|
1952
|
+
currentAppId: authoritativeLane?.currentAppId ?? String(app.id),
|
|
1953
|
+
upstreamAppId: authoritativeLane?.upstreamAppId ?? upstreamAppId,
|
|
1954
|
+
threadId: authoritativeLane?.threadId ?? (app.threadId ? String(app.threadId) : null),
|
|
1955
|
+
repoFingerprint: authoritativeLane?.repoFingerprint ?? repoFingerprint,
|
|
1956
|
+
remoteUrl: authoritativeLane?.remoteUrl ?? remoteUrl,
|
|
1957
|
+
defaultBranch: authoritativeLane?.defaultBranch ?? defaultBranch,
|
|
1958
|
+
laneId: authoritativeLane?.laneId ?? laneId,
|
|
1959
|
+
branchName: authoritativeLane?.branchName ?? branchName,
|
|
1960
|
+
bindingMode: "lane"
|
|
1553
1961
|
});
|
|
1554
1962
|
return {
|
|
1555
|
-
appId: String(app.id),
|
|
1556
|
-
dashboardUrl: buildDashboardAppUrl(String(app.id)),
|
|
1557
|
-
projectId: String(app.projectId),
|
|
1558
|
-
upstreamAppId,
|
|
1963
|
+
appId: authoritativeLane?.currentAppId ?? String(app.id),
|
|
1964
|
+
dashboardUrl: buildDashboardAppUrl(authoritativeLane?.currentAppId ?? String(app.id)),
|
|
1965
|
+
projectId: authoritativeLane?.projectId ?? String(app.projectId),
|
|
1966
|
+
upstreamAppId: authoritativeLane?.upstreamAppId ?? upstreamAppId,
|
|
1559
1967
|
bindingPath,
|
|
1560
1968
|
repoRoot: checkout.repoRoot
|
|
1561
1969
|
};
|
|
@@ -1577,14 +1985,32 @@ async function resolveQueueAppId(params) {
|
|
|
1577
1985
|
});
|
|
1578
1986
|
}
|
|
1579
1987
|
const repoRoot = await findGitRoot(params.cwd);
|
|
1580
|
-
const
|
|
1581
|
-
if (
|
|
1988
|
+
const bindingResolution = await resolveActiveLaneBinding({ repoRoot });
|
|
1989
|
+
if (bindingResolution.status === "not_bound") {
|
|
1582
1990
|
throw new RemixError("Repository is not bound to Remix.", {
|
|
1583
1991
|
exitCode: 2,
|
|
1584
1992
|
hint: "Bind the repository first or pass `appId` explicitly for the app-scoped merge-request queue."
|
|
1585
1993
|
});
|
|
1586
1994
|
}
|
|
1587
|
-
|
|
1995
|
+
if (bindingResolution.status === "missing_branch_binding") {
|
|
1996
|
+
throw new RemixError("Current branch is not yet bound to a Remix lane.", {
|
|
1997
|
+
exitCode: 2,
|
|
1998
|
+
hint: `Switch back to a bound branch or create a lane by recording work on ${bindingResolution.currentBranch ?? "this branch"}.`
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
if (bindingResolution.status === "binding_conflict") {
|
|
2002
|
+
throw new RemixError("Current branch binding conflicts with the server-resolved Remix lane.", {
|
|
2003
|
+
exitCode: 2,
|
|
2004
|
+
hint: `Local app ${bindingResolution.binding.currentAppId}; server app ${bindingResolution.resolvedLane.currentAppId ?? "(unknown)"}.`
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
if (bindingResolution.status === "ambiguous_family_selection") {
|
|
2008
|
+
throw new RemixError("Multiple canonical Remix families match this repository.", {
|
|
2009
|
+
exitCode: 2,
|
|
2010
|
+
hint: "This checkout does not identify a single canonical family. Continue from a checkout already bound to the intended family, or pass `appId` explicitly for the queue request."
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
return bindingResolution.binding.currentAppId;
|
|
1588
2014
|
}
|
|
1589
2015
|
async function collabListMergeRequests(params) {
|
|
1590
2016
|
const appId = await resolveQueueAppId({
|
|
@@ -1615,12 +2041,38 @@ async function collabListMergeRequests(params) {
|
|
|
1615
2041
|
async function resolveScopeTarget(params) {
|
|
1616
2042
|
if (params.targetId?.trim()) return params.targetId.trim();
|
|
1617
2043
|
const repoRoot = await findGitRoot(params.cwd);
|
|
1618
|
-
const
|
|
1619
|
-
if (
|
|
2044
|
+
const bindingResolution = await resolveActiveLaneBinding({ repoRoot, api: params.api });
|
|
2045
|
+
if (bindingResolution.status === "not_bound") {
|
|
1620
2046
|
throw new RemixError("Repository is not bound to Remix and no explicit target id was provided.", { exitCode: 2 });
|
|
1621
2047
|
}
|
|
1622
|
-
if (
|
|
2048
|
+
if (bindingResolution.status === "missing_branch_binding") {
|
|
2049
|
+
throw new RemixError("Current branch is not yet bound to a Remix lane and no explicit target id was provided.", {
|
|
2050
|
+
exitCode: 2
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
if (bindingResolution.status === "binding_conflict") {
|
|
2054
|
+
throw new RemixError("Current branch binding conflicts with the server-resolved Remix lane.", {
|
|
2055
|
+
exitCode: 2,
|
|
2056
|
+
hint: `Local app ${bindingResolution.binding.currentAppId}; server app ${bindingResolution.resolvedLane.currentAppId ?? "(unknown)"}.`
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
if (bindingResolution.status === "ambiguous_family_selection") {
|
|
2060
|
+
throw new RemixError("Multiple canonical Remix families match this repository and no explicit target id was provided.", {
|
|
2061
|
+
exitCode: 2,
|
|
2062
|
+
hint: "This checkout does not identify a single canonical family. Continue from a checkout already bound to the intended family, or pass `targetId` explicitly."
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
const binding = bindingResolution.binding;
|
|
2066
|
+
if (params.scope === "project") {
|
|
2067
|
+
if (!binding.projectId) {
|
|
2068
|
+
throw new RemixError("Could not resolve the project for the current repository binding.", { exitCode: 2 });
|
|
2069
|
+
}
|
|
2070
|
+
return binding.projectId;
|
|
2071
|
+
}
|
|
1623
2072
|
if (params.scope === "app") return binding.currentAppId;
|
|
2073
|
+
if (!binding.projectId) {
|
|
2074
|
+
throw new RemixError("Could not resolve the project for the current repository binding.", { exitCode: 2 });
|
|
2075
|
+
}
|
|
1624
2076
|
const project = unwrapResponseObject(await params.api.getProject(binding.projectId), "project");
|
|
1625
2077
|
const organizationId = typeof project.organizationId === "string" ? project.organizationId : null;
|
|
1626
2078
|
if (!organizationId) {
|
|
@@ -1740,6 +2192,85 @@ ${text}`.trim() || null
|
|
|
1740
2192
|
}
|
|
1741
2193
|
|
|
1742
2194
|
// src/application/collab/collabInit.ts
|
|
2195
|
+
function requireResolvedLaneBinding(lane, params) {
|
|
2196
|
+
if (lane.status === "resolved" && lane.currentAppId && lane.upstreamAppId) {
|
|
2197
|
+
return lane;
|
|
2198
|
+
}
|
|
2199
|
+
const branchLabel = params.branchName ?? "the current branch";
|
|
2200
|
+
const laneStatus = String(lane.status ?? "");
|
|
2201
|
+
throw new RemixError(`Failed to resolve a Remix lane for ${branchLabel}.`, {
|
|
2202
|
+
exitCode: 1,
|
|
2203
|
+
hint: laneStatus === "binding_not_found" ? `Run ${params.operation} again after the repository has been initialized.` : laneStatus === "ambiguous_family_selection" ? "Multiple canonical Remix families match this repository. Continue from a checkout already bound to the intended family, or rerun with `--force-new` to create a new family." : `Remix did not return complete lane metadata for ${branchLabel}.`
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
function resolveProjectBindingResult(response) {
|
|
2207
|
+
const payload = response?.responseObject;
|
|
2208
|
+
if (!payload || typeof payload !== "object") {
|
|
2209
|
+
return { status: "not_found" };
|
|
2210
|
+
}
|
|
2211
|
+
if (payload.status === "ambiguous_family_selection") {
|
|
2212
|
+
const projectIds = Array.isArray(payload.projectIds) ? payload.projectIds.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
|
|
2213
|
+
return {
|
|
2214
|
+
status: "ambiguous_family_selection",
|
|
2215
|
+
candidateCount: typeof payload.candidateCount === "number" && Number.isFinite(payload.candidateCount) ? payload.candidateCount : projectIds.length,
|
|
2216
|
+
projectIds
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
if (payload.projectId && payload.appId) {
|
|
2220
|
+
return {
|
|
2221
|
+
status: "resolved",
|
|
2222
|
+
projectId: String(payload.projectId),
|
|
2223
|
+
appId: String(payload.appId),
|
|
2224
|
+
upstreamAppId: String(payload.upstreamAppId ?? payload.appId),
|
|
2225
|
+
threadId: payload.threadId ? String(payload.threadId) : null
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
return { status: "not_found" };
|
|
2229
|
+
}
|
|
2230
|
+
function throwAmbiguousFamilyError(params) {
|
|
2231
|
+
const familyCount = params.candidateCount || params.projectIds.length;
|
|
2232
|
+
const projectHint = params.projectIds.length > 0 ? ` Matching project ids: ${params.projectIds.join(", ")}.` : "";
|
|
2233
|
+
throw new RemixError("Multiple canonical Remix families already match this repository.", {
|
|
2234
|
+
exitCode: 2,
|
|
2235
|
+
hint: `Plain \`remix collab init\` cannot safely choose among ${familyCount} matching canonical families for repo fingerprint ${params.repoFingerprint}${params.remoteUrl ? ` (${params.remoteUrl})` : ""}.${projectHint} Run \`remix collab init --force-new\` to create a new canonical family, or continue from a checkout already bound to the intended family.`
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
async function resolveOrEnsureLaneBinding(params) {
|
|
2239
|
+
const resolvePayload = {
|
|
2240
|
+
projectId: params.projectId ?? void 0,
|
|
2241
|
+
repoFingerprint: params.repoFingerprint,
|
|
2242
|
+
remoteUrl: params.remoteUrl ?? void 0,
|
|
2243
|
+
defaultBranch: params.defaultBranch ?? void 0,
|
|
2244
|
+
branchName: params.branchName
|
|
2245
|
+
};
|
|
2246
|
+
let lane = unwrapResponseObject(
|
|
2247
|
+
await params.api.resolveProjectLaneBinding(resolvePayload),
|
|
2248
|
+
"project lane binding"
|
|
2249
|
+
);
|
|
2250
|
+
if (lane.status !== "resolved") {
|
|
2251
|
+
lane = unwrapResponseObject(
|
|
2252
|
+
await params.api.ensureProjectLaneBinding({
|
|
2253
|
+
...resolvePayload,
|
|
2254
|
+
seedAppId: params.seedAppId ?? void 0
|
|
2255
|
+
}),
|
|
2256
|
+
"project lane binding"
|
|
2257
|
+
);
|
|
2258
|
+
}
|
|
2259
|
+
return requireResolvedLaneBinding(lane, {
|
|
2260
|
+
branchName: params.branchName,
|
|
2261
|
+
operation: params.operation
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2264
|
+
function branchBindingFromLane(lane, mode, fallback) {
|
|
2265
|
+
return {
|
|
2266
|
+
projectId: lane.projectId ?? fallback.projectId,
|
|
2267
|
+
currentAppId: lane.currentAppId ?? fallback.currentAppId,
|
|
2268
|
+
upstreamAppId: lane.upstreamAppId ?? fallback.upstreamAppId,
|
|
2269
|
+
threadId: lane.threadId ?? fallback.threadId,
|
|
2270
|
+
laneId: lane.laneId ?? null,
|
|
2271
|
+
bindingMode: mode
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
1743
2274
|
async function collabInit(params) {
|
|
1744
2275
|
return withRepoMutationLock(
|
|
1745
2276
|
{
|
|
@@ -1758,38 +2289,220 @@ async function collabInit(params) {
|
|
|
1758
2289
|
const remoteUrl = normalizeGitRemote(await getRemoteOriginUrl(repoRoot));
|
|
1759
2290
|
const currentBranch = await getCurrentBranch(repoRoot);
|
|
1760
2291
|
const defaultBranch = await getDefaultBranch(repoRoot) ?? currentBranch;
|
|
1761
|
-
const
|
|
2292
|
+
const branchName = currentBranch ?? defaultBranch ?? null;
|
|
1762
2293
|
const repoFingerprint = await buildRepoFingerprint({ gitRoot: repoRoot, remoteUrl, defaultBranch });
|
|
1763
2294
|
const repoSnapshot = await captureRepoSnapshot(repoRoot);
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
2295
|
+
const localBindingState = await readCollabBindingState(repoRoot, { persist: true });
|
|
2296
|
+
if (!params.forceNew && localBindingState?.explicitRootBinding && branchName) {
|
|
2297
|
+
const explicitRoot = localBindingState.explicitRootBinding;
|
|
2298
|
+
const explicitProjectId = explicitRoot.projectId ?? localBindingState.projectId;
|
|
2299
|
+
let canonicalLane2 = null;
|
|
2300
|
+
let boundProjectId2 = explicitProjectId;
|
|
2301
|
+
let boundCurrentAppId2 = explicitRoot.currentAppId;
|
|
2302
|
+
let boundUpstreamAppId2 = explicitRoot.upstreamAppId;
|
|
2303
|
+
let boundThreadId2 = explicitRoot.threadId;
|
|
2304
|
+
let boundLaneId2 = explicitRoot.laneId;
|
|
2305
|
+
if (defaultBranch && branchName !== defaultBranch) {
|
|
2306
|
+
canonicalLane2 = await resolveOrEnsureLaneBinding({
|
|
2307
|
+
api: params.api,
|
|
2308
|
+
projectId: explicitProjectId ?? void 0,
|
|
2309
|
+
repoFingerprint,
|
|
2310
|
+
remoteUrl,
|
|
2311
|
+
defaultBranch,
|
|
2312
|
+
branchName: defaultBranch,
|
|
2313
|
+
operation: "`remix collab init`"
|
|
2314
|
+
});
|
|
2315
|
+
const lane = await resolveOrEnsureLaneBinding({
|
|
2316
|
+
api: params.api,
|
|
2317
|
+
projectId: canonicalLane2.projectId ?? explicitProjectId ?? void 0,
|
|
2318
|
+
repoFingerprint,
|
|
2319
|
+
remoteUrl,
|
|
2320
|
+
defaultBranch,
|
|
2321
|
+
branchName,
|
|
2322
|
+
operation: "`remix collab init`"
|
|
2323
|
+
});
|
|
2324
|
+
boundProjectId2 = lane.projectId ?? boundProjectId2;
|
|
2325
|
+
boundCurrentAppId2 = lane.currentAppId ?? boundCurrentAppId2;
|
|
2326
|
+
boundUpstreamAppId2 = lane.upstreamAppId ?? boundUpstreamAppId2;
|
|
2327
|
+
boundThreadId2 = lane.threadId ?? boundThreadId2;
|
|
2328
|
+
boundLaneId2 = lane.laneId ?? null;
|
|
2329
|
+
} else {
|
|
2330
|
+
canonicalLane2 = await resolveOrEnsureLaneBinding({
|
|
2331
|
+
api: params.api,
|
|
2332
|
+
projectId: explicitProjectId ?? void 0,
|
|
2333
|
+
repoFingerprint,
|
|
2334
|
+
remoteUrl,
|
|
2335
|
+
defaultBranch,
|
|
2336
|
+
branchName,
|
|
2337
|
+
operation: "`remix collab init`"
|
|
2338
|
+
});
|
|
2339
|
+
boundProjectId2 = canonicalLane2.projectId ?? boundProjectId2;
|
|
2340
|
+
boundCurrentAppId2 = canonicalLane2.currentAppId ?? boundCurrentAppId2;
|
|
2341
|
+
boundUpstreamAppId2 = canonicalLane2.upstreamAppId ?? boundUpstreamAppId2;
|
|
2342
|
+
boundThreadId2 = canonicalLane2.threadId ?? boundThreadId2;
|
|
2343
|
+
boundLaneId2 = canonicalLane2.laneId ?? null;
|
|
2344
|
+
}
|
|
2345
|
+
const readyApp = await pollAppReady(params.api, boundCurrentAppId2);
|
|
2346
|
+
boundProjectId2 = String(readyApp.projectId ?? boundProjectId2);
|
|
2347
|
+
boundThreadId2 = readyApp.threadId ? String(readyApp.threadId) : boundThreadId2;
|
|
2348
|
+
await assertRepoSnapshotUnchanged(repoRoot, repoSnapshot, {
|
|
2349
|
+
operation: "`remix collab init`",
|
|
2350
|
+
recoveryHint: "The repository changed while the local binding was being initialized. Review the local changes and rerun `remix collab init`."
|
|
2351
|
+
});
|
|
2352
|
+
await writeCollabBinding(repoRoot, {
|
|
2353
|
+
projectId: canonicalLane2?.projectId ?? explicitProjectId ?? null,
|
|
2354
|
+
currentAppId: canonicalLane2?.currentAppId ?? explicitRoot.currentAppId,
|
|
2355
|
+
upstreamAppId: canonicalLane2?.upstreamAppId ?? canonicalLane2?.currentAppId ?? explicitRoot.upstreamAppId ?? explicitRoot.currentAppId,
|
|
2356
|
+
threadId: canonicalLane2?.threadId ?? explicitRoot.threadId,
|
|
2357
|
+
repoFingerprint: canonicalLane2?.repoFingerprint ?? explicitRoot.repoFingerprint ?? repoFingerprint,
|
|
2358
|
+
remoteUrl: canonicalLane2?.remoteUrl ?? explicitRoot.remoteUrl ?? remoteUrl,
|
|
2359
|
+
defaultBranch: canonicalLane2?.defaultBranch ?? explicitRoot.defaultBranch ?? defaultBranch ?? null,
|
|
2360
|
+
laneId: canonicalLane2?.laneId ?? explicitRoot.laneId,
|
|
2361
|
+
branchName: defaultBranch,
|
|
2362
|
+
bindingMode: "explicit_root"
|
|
1768
2363
|
});
|
|
1769
|
-
|
|
1770
|
-
|
|
2364
|
+
if (defaultBranch && branchName !== defaultBranch) {
|
|
2365
|
+
await writeCollabBinding(repoRoot, {
|
|
2366
|
+
projectId: boundProjectId2,
|
|
2367
|
+
currentAppId: boundCurrentAppId2,
|
|
2368
|
+
upstreamAppId: boundUpstreamAppId2,
|
|
2369
|
+
threadId: boundThreadId2,
|
|
2370
|
+
repoFingerprint,
|
|
2371
|
+
remoteUrl,
|
|
2372
|
+
defaultBranch: defaultBranch ?? null,
|
|
2373
|
+
laneId: boundLaneId2,
|
|
2374
|
+
branchName,
|
|
2375
|
+
bindingMode: "lane"
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
return {
|
|
2379
|
+
reused: true,
|
|
2380
|
+
projectId: boundProjectId2 ?? explicitProjectId ?? "",
|
|
2381
|
+
appId: boundCurrentAppId2,
|
|
2382
|
+
dashboardUrl: buildDashboardAppUrl(boundCurrentAppId2),
|
|
2383
|
+
upstreamAppId: boundUpstreamAppId2,
|
|
2384
|
+
bindingPath: path5.join(repoRoot, ".remix", "config.json"),
|
|
2385
|
+
repoRoot,
|
|
2386
|
+
bindingMode: defaultBranch && branchName !== defaultBranch ? "lane" : "explicit_root",
|
|
2387
|
+
createdCanonicalFamily: false,
|
|
2388
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
2389
|
+
};
|
|
2390
|
+
}
|
|
2391
|
+
if (!params.forceNew) {
|
|
2392
|
+
const bindingResolution = resolveProjectBindingResult(
|
|
2393
|
+
await params.api.resolveProjectBinding({
|
|
2394
|
+
repoFingerprint,
|
|
2395
|
+
remoteUrl: remoteUrl ?? void 0,
|
|
2396
|
+
branchName: branchName ?? void 0
|
|
2397
|
+
})
|
|
2398
|
+
);
|
|
2399
|
+
if (bindingResolution.status === "ambiguous_family_selection") {
|
|
2400
|
+
throwAmbiguousFamilyError({
|
|
2401
|
+
repoFingerprint,
|
|
2402
|
+
remoteUrl,
|
|
2403
|
+
projectIds: bindingResolution.projectIds,
|
|
2404
|
+
candidateCount: bindingResolution.candidateCount
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2407
|
+
if (bindingResolution.status === "resolved") {
|
|
2408
|
+
const initialProjectId = bindingResolution.projectId;
|
|
2409
|
+
const initialCurrentAppId = bindingResolution.appId;
|
|
2410
|
+
const initialUpstreamAppId = bindingResolution.upstreamAppId;
|
|
2411
|
+
const initialThreadId = bindingResolution.threadId;
|
|
2412
|
+
let boundProjectId2 = initialProjectId;
|
|
2413
|
+
let boundCurrentAppId2 = initialCurrentAppId;
|
|
2414
|
+
let boundUpstreamAppId2 = initialUpstreamAppId;
|
|
2415
|
+
let boundThreadId2 = initialThreadId;
|
|
2416
|
+
let boundLaneId2 = null;
|
|
2417
|
+
let canonicalLane2 = null;
|
|
2418
|
+
if (branchName) {
|
|
2419
|
+
if (defaultBranch && branchName !== defaultBranch) {
|
|
2420
|
+
canonicalLane2 = await resolveOrEnsureLaneBinding({
|
|
2421
|
+
api: params.api,
|
|
2422
|
+
repoFingerprint,
|
|
2423
|
+
remoteUrl,
|
|
2424
|
+
defaultBranch,
|
|
2425
|
+
branchName: defaultBranch,
|
|
2426
|
+
seedAppId: initialCurrentAppId,
|
|
2427
|
+
operation: "`remix collab init`"
|
|
2428
|
+
});
|
|
2429
|
+
const lane = await resolveOrEnsureLaneBinding({
|
|
2430
|
+
api: params.api,
|
|
2431
|
+
projectId: canonicalLane2.projectId ?? void 0,
|
|
2432
|
+
repoFingerprint,
|
|
2433
|
+
remoteUrl,
|
|
2434
|
+
defaultBranch,
|
|
2435
|
+
branchName,
|
|
2436
|
+
operation: "`remix collab init`"
|
|
2437
|
+
});
|
|
2438
|
+
boundProjectId2 = lane.projectId ?? boundProjectId2;
|
|
2439
|
+
boundCurrentAppId2 = lane.currentAppId ?? boundCurrentAppId2;
|
|
2440
|
+
boundUpstreamAppId2 = lane.upstreamAppId ?? boundUpstreamAppId2;
|
|
2441
|
+
boundThreadId2 = lane.threadId ?? boundThreadId2;
|
|
2442
|
+
boundLaneId2 = lane.laneId ?? null;
|
|
2443
|
+
} else {
|
|
2444
|
+
const lane = await resolveOrEnsureLaneBinding({
|
|
2445
|
+
api: params.api,
|
|
2446
|
+
repoFingerprint,
|
|
2447
|
+
remoteUrl,
|
|
2448
|
+
defaultBranch,
|
|
2449
|
+
branchName,
|
|
2450
|
+
seedAppId: initialCurrentAppId,
|
|
2451
|
+
operation: "`remix collab init`"
|
|
2452
|
+
});
|
|
2453
|
+
canonicalLane2 = lane;
|
|
2454
|
+
boundProjectId2 = lane.projectId ?? boundProjectId2;
|
|
2455
|
+
boundCurrentAppId2 = lane.currentAppId ?? boundCurrentAppId2;
|
|
2456
|
+
boundUpstreamAppId2 = lane.upstreamAppId ?? boundUpstreamAppId2;
|
|
2457
|
+
boundThreadId2 = lane.threadId ?? boundThreadId2;
|
|
2458
|
+
boundLaneId2 = lane.laneId ?? null;
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
if (boundCurrentAppId2) {
|
|
2462
|
+
const readyApp = await pollAppReady(params.api, boundCurrentAppId2);
|
|
2463
|
+
boundProjectId2 = String(readyApp.projectId ?? boundProjectId2);
|
|
2464
|
+
boundThreadId2 = readyApp.threadId ? String(readyApp.threadId) : boundThreadId2;
|
|
2465
|
+
}
|
|
1771
2466
|
await assertRepoSnapshotUnchanged(repoRoot, repoSnapshot, {
|
|
1772
2467
|
operation: "`remix collab init`",
|
|
1773
2468
|
recoveryHint: "The repository changed while the local binding was being initialized. Review the local changes and rerun `remix collab init`."
|
|
1774
2469
|
});
|
|
2470
|
+
if (canonicalLane2 && defaultBranch && branchName && branchName !== defaultBranch) {
|
|
2471
|
+
await writeCollabBinding(repoRoot, {
|
|
2472
|
+
projectId: canonicalLane2.projectId ?? null,
|
|
2473
|
+
currentAppId: canonicalLane2.currentAppId ?? boundCurrentAppId2,
|
|
2474
|
+
upstreamAppId: canonicalLane2.upstreamAppId ?? canonicalLane2.currentAppId ?? boundCurrentAppId2,
|
|
2475
|
+
threadId: canonicalLane2.threadId ?? null,
|
|
2476
|
+
repoFingerprint: canonicalLane2.repoFingerprint ?? repoFingerprint,
|
|
2477
|
+
remoteUrl: canonicalLane2.remoteUrl ?? remoteUrl,
|
|
2478
|
+
defaultBranch: canonicalLane2.defaultBranch ?? defaultBranch,
|
|
2479
|
+
laneId: canonicalLane2.laneId ?? null,
|
|
2480
|
+
branchName: defaultBranch,
|
|
2481
|
+
bindingMode: "lane"
|
|
2482
|
+
});
|
|
2483
|
+
}
|
|
1775
2484
|
const bindingPath2 = await writeCollabBinding(repoRoot, {
|
|
1776
|
-
projectId:
|
|
1777
|
-
currentAppId:
|
|
1778
|
-
upstreamAppId:
|
|
1779
|
-
threadId:
|
|
2485
|
+
projectId: boundProjectId2,
|
|
2486
|
+
currentAppId: boundCurrentAppId2,
|
|
2487
|
+
upstreamAppId: boundUpstreamAppId2,
|
|
2488
|
+
threadId: boundThreadId2,
|
|
1780
2489
|
repoFingerprint,
|
|
1781
2490
|
remoteUrl,
|
|
1782
2491
|
defaultBranch: defaultBranch ?? null,
|
|
1783
|
-
|
|
2492
|
+
laneId: boundLaneId2,
|
|
2493
|
+
branchName,
|
|
2494
|
+
bindingMode: "lane"
|
|
1784
2495
|
});
|
|
1785
2496
|
return {
|
|
1786
2497
|
reused: true,
|
|
1787
|
-
projectId:
|
|
1788
|
-
appId:
|
|
1789
|
-
dashboardUrl: buildDashboardAppUrl(
|
|
1790
|
-
upstreamAppId:
|
|
2498
|
+
projectId: boundProjectId2,
|
|
2499
|
+
appId: boundCurrentAppId2,
|
|
2500
|
+
dashboardUrl: buildDashboardAppUrl(boundCurrentAppId2),
|
|
2501
|
+
upstreamAppId: boundUpstreamAppId2,
|
|
1791
2502
|
bindingPath: bindingPath2,
|
|
1792
2503
|
repoRoot,
|
|
2504
|
+
bindingMode: "lane",
|
|
2505
|
+
createdCanonicalFamily: false,
|
|
1793
2506
|
...warnings.length > 0 ? { warnings } : {}
|
|
1794
2507
|
};
|
|
1795
2508
|
}
|
|
@@ -1817,6 +2530,7 @@ async function collabInit(params) {
|
|
|
1817
2530
|
path: params.path?.trim() || void 0,
|
|
1818
2531
|
platform: "generic",
|
|
1819
2532
|
isPublic: false,
|
|
2533
|
+
branch: defaultBranch && branchName && branchName !== defaultBranch ? defaultBranch : currentBranch ?? void 0,
|
|
1820
2534
|
remoteUrl: remoteUrl ?? void 0,
|
|
1821
2535
|
defaultBranch: defaultBranch ?? void 0,
|
|
1822
2536
|
repoFingerprint,
|
|
@@ -1824,31 +2538,136 @@ async function collabInit(params) {
|
|
|
1824
2538
|
});
|
|
1825
2539
|
const imported = unwrapResponseObject(importResp, "import");
|
|
1826
2540
|
const app = await pollAppReady(params.api, String(imported.appId));
|
|
2541
|
+
let boundProjectId = String(app.projectId);
|
|
2542
|
+
let boundCurrentAppId = String(app.id);
|
|
2543
|
+
let boundUpstreamAppId = String(app.id);
|
|
2544
|
+
let boundThreadId = app.threadId ? String(app.threadId) : null;
|
|
2545
|
+
let boundLaneId = null;
|
|
2546
|
+
let canonicalLane = null;
|
|
2547
|
+
if (branchName) {
|
|
2548
|
+
if (defaultBranch && branchName !== defaultBranch) {
|
|
2549
|
+
canonicalLane = await resolveOrEnsureLaneBinding({
|
|
2550
|
+
api: params.api,
|
|
2551
|
+
projectId: boundProjectId,
|
|
2552
|
+
repoFingerprint,
|
|
2553
|
+
remoteUrl,
|
|
2554
|
+
defaultBranch,
|
|
2555
|
+
branchName: defaultBranch,
|
|
2556
|
+
seedAppId: String(app.id),
|
|
2557
|
+
operation: "`remix collab init`"
|
|
2558
|
+
});
|
|
2559
|
+
const lane = await resolveOrEnsureLaneBinding({
|
|
2560
|
+
api: params.api,
|
|
2561
|
+
projectId: canonicalLane.projectId ?? boundProjectId,
|
|
2562
|
+
repoFingerprint,
|
|
2563
|
+
remoteUrl,
|
|
2564
|
+
defaultBranch,
|
|
2565
|
+
branchName,
|
|
2566
|
+
operation: "`remix collab init`"
|
|
2567
|
+
});
|
|
2568
|
+
boundProjectId = lane.projectId ?? boundProjectId;
|
|
2569
|
+
boundCurrentAppId = lane.currentAppId ?? boundCurrentAppId;
|
|
2570
|
+
boundUpstreamAppId = lane.upstreamAppId ?? boundUpstreamAppId;
|
|
2571
|
+
boundThreadId = lane.threadId ?? boundThreadId;
|
|
2572
|
+
boundLaneId = lane.laneId ?? null;
|
|
2573
|
+
} else {
|
|
2574
|
+
const lane = await resolveOrEnsureLaneBinding({
|
|
2575
|
+
api: params.api,
|
|
2576
|
+
projectId: boundProjectId,
|
|
2577
|
+
repoFingerprint,
|
|
2578
|
+
remoteUrl,
|
|
2579
|
+
defaultBranch,
|
|
2580
|
+
branchName,
|
|
2581
|
+
seedAppId: String(app.id),
|
|
2582
|
+
operation: "`remix collab init`"
|
|
2583
|
+
});
|
|
2584
|
+
canonicalLane = lane;
|
|
2585
|
+
boundProjectId = lane.projectId ?? boundProjectId;
|
|
2586
|
+
boundCurrentAppId = lane.currentAppId ?? boundCurrentAppId;
|
|
2587
|
+
boundUpstreamAppId = lane.upstreamAppId ?? boundUpstreamAppId;
|
|
2588
|
+
boundThreadId = lane.threadId ?? boundThreadId;
|
|
2589
|
+
boundLaneId = lane.laneId ?? null;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
if (boundCurrentAppId) {
|
|
2593
|
+
const readyApp = await pollAppReady(params.api, boundCurrentAppId);
|
|
2594
|
+
boundProjectId = String(readyApp.projectId ?? boundProjectId);
|
|
2595
|
+
boundThreadId = readyApp.threadId ? String(readyApp.threadId) : boundThreadId;
|
|
2596
|
+
}
|
|
1827
2597
|
await assertRepoSnapshotUnchanged(repoRoot, repoSnapshot, {
|
|
1828
2598
|
operation: "`remix collab init`",
|
|
1829
2599
|
recoveryHint: "The repository changed before the Remix binding was written. Review the local changes and rerun `remix collab init`."
|
|
1830
2600
|
});
|
|
1831
|
-
const
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
2601
|
+
const bindingMode = params.forceNew && (!defaultBranch || branchName === defaultBranch) ? "explicit_root" : "lane";
|
|
2602
|
+
let bindingPath;
|
|
2603
|
+
if (params.forceNew && defaultBranch && canonicalLane) {
|
|
2604
|
+
const canonicalBinding = branchBindingFromLane(canonicalLane, "explicit_root", {
|
|
2605
|
+
projectId: canonicalLane.projectId ?? boundProjectId,
|
|
2606
|
+
currentAppId: canonicalLane.currentAppId ?? boundCurrentAppId,
|
|
2607
|
+
upstreamAppId: canonicalLane.upstreamAppId ?? canonicalLane.currentAppId ?? boundCurrentAppId,
|
|
2608
|
+
threadId: canonicalLane.threadId ?? boundThreadId
|
|
2609
|
+
});
|
|
2610
|
+
const branchBindings = {
|
|
2611
|
+
[defaultBranch]: canonicalBinding
|
|
2612
|
+
};
|
|
2613
|
+
if (branchName && branchName !== defaultBranch) {
|
|
2614
|
+
branchBindings[branchName] = {
|
|
2615
|
+
projectId: boundProjectId,
|
|
2616
|
+
currentAppId: boundCurrentAppId,
|
|
2617
|
+
upstreamAppId: boundUpstreamAppId,
|
|
2618
|
+
threadId: boundThreadId,
|
|
2619
|
+
laneId: boundLaneId,
|
|
2620
|
+
bindingMode: "lane"
|
|
2621
|
+
};
|
|
2622
|
+
}
|
|
2623
|
+
bindingPath = await writeCollabBindingSnapshot({
|
|
2624
|
+
repoRoot,
|
|
2625
|
+
repoFingerprint,
|
|
2626
|
+
remoteUrl,
|
|
2627
|
+
defaultBranch,
|
|
2628
|
+
branchBindings,
|
|
2629
|
+
explicitRootBinding: canonicalBinding
|
|
2630
|
+
});
|
|
2631
|
+
} else {
|
|
2632
|
+
if (canonicalLane && defaultBranch && branchName && branchName !== defaultBranch) {
|
|
2633
|
+
await writeCollabBinding(repoRoot, {
|
|
2634
|
+
projectId: canonicalLane.projectId ?? null,
|
|
2635
|
+
currentAppId: canonicalLane.currentAppId ?? boundCurrentAppId,
|
|
2636
|
+
upstreamAppId: canonicalLane.upstreamAppId ?? canonicalLane.currentAppId ?? boundCurrentAppId,
|
|
2637
|
+
threadId: canonicalLane.threadId ?? null,
|
|
2638
|
+
repoFingerprint: canonicalLane.repoFingerprint ?? repoFingerprint,
|
|
2639
|
+
remoteUrl: canonicalLane.remoteUrl ?? remoteUrl,
|
|
2640
|
+
defaultBranch: canonicalLane.defaultBranch ?? defaultBranch,
|
|
2641
|
+
laneId: canonicalLane.laneId ?? null,
|
|
2642
|
+
branchName: defaultBranch,
|
|
2643
|
+
bindingMode: params.forceNew ? "explicit_root" : "lane"
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
bindingPath = await writeCollabBinding(repoRoot, {
|
|
2647
|
+
projectId: boundProjectId,
|
|
2648
|
+
currentAppId: boundCurrentAppId,
|
|
2649
|
+
upstreamAppId: boundUpstreamAppId,
|
|
2650
|
+
threadId: boundThreadId,
|
|
2651
|
+
repoFingerprint,
|
|
2652
|
+
remoteUrl,
|
|
2653
|
+
defaultBranch: defaultBranch ?? null,
|
|
2654
|
+
laneId: boundLaneId,
|
|
2655
|
+
branchName,
|
|
2656
|
+
bindingMode
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
1841
2659
|
return {
|
|
1842
2660
|
reused: false,
|
|
1843
|
-
projectId:
|
|
1844
|
-
appId:
|
|
1845
|
-
dashboardUrl: buildDashboardAppUrl(
|
|
1846
|
-
upstreamAppId:
|
|
2661
|
+
projectId: boundProjectId,
|
|
2662
|
+
appId: boundCurrentAppId,
|
|
2663
|
+
dashboardUrl: buildDashboardAppUrl(boundCurrentAppId),
|
|
2664
|
+
upstreamAppId: boundUpstreamAppId,
|
|
1847
2665
|
bindingPath,
|
|
1848
2666
|
repoRoot,
|
|
2667
|
+
bindingMode,
|
|
2668
|
+
createdCanonicalFamily: Boolean(params.forceNew),
|
|
1849
2669
|
remoteUrl,
|
|
1850
2670
|
defaultBranch,
|
|
1851
|
-
preferredBranch,
|
|
1852
2671
|
...warnings.length > 0 ? { warnings } : {}
|
|
1853
2672
|
};
|
|
1854
2673
|
}
|
|
@@ -1903,7 +2722,11 @@ import os4 from "os";
|
|
|
1903
2722
|
import path6 from "path";
|
|
1904
2723
|
async function collabReconcile(params) {
|
|
1905
2724
|
const repoRoot = await findGitRoot(params.cwd);
|
|
1906
|
-
const binding = await
|
|
2725
|
+
const binding = await ensureActiveLaneBinding({
|
|
2726
|
+
repoRoot,
|
|
2727
|
+
api: params.api,
|
|
2728
|
+
operation: "`remix collab reconcile`"
|
|
2729
|
+
});
|
|
1907
2730
|
if (!binding) {
|
|
1908
2731
|
throw new RemixError("Repository is not bound to Remix.", {
|
|
1909
2732
|
exitCode: 2,
|
|
@@ -1912,9 +2735,9 @@ async function collabReconcile(params) {
|
|
|
1912
2735
|
}
|
|
1913
2736
|
await ensureCleanWorktree(repoRoot, "`remix collab reconcile`");
|
|
1914
2737
|
const branch = await requireCurrentBranch(repoRoot);
|
|
1915
|
-
|
|
2738
|
+
assertBoundBranchMatch({
|
|
1916
2739
|
currentBranch: branch,
|
|
1917
|
-
|
|
2740
|
+
branchName: binding.branchName,
|
|
1918
2741
|
allowBranchMismatch: params.allowBranchMismatch,
|
|
1919
2742
|
operation: "`remix collab reconcile`"
|
|
1920
2743
|
});
|
|
@@ -2023,9 +2846,9 @@ async function collabReconcile(params) {
|
|
|
2023
2846
|
});
|
|
2024
2847
|
await ensureCleanWorktree(lockedRepoRoot, "`remix collab reconcile`");
|
|
2025
2848
|
const lockedBranch = await requireCurrentBranch(lockedRepoRoot);
|
|
2026
|
-
|
|
2849
|
+
assertBoundBranchMatch({
|
|
2027
2850
|
currentBranch: lockedBranch,
|
|
2028
|
-
|
|
2851
|
+
branchName: binding.branchName,
|
|
2029
2852
|
allowBranchMismatch: params.allowBranchMismatch,
|
|
2030
2853
|
operation: "`remix collab reconcile`"
|
|
2031
2854
|
});
|
|
@@ -2084,31 +2907,65 @@ async function collabRemix(params) {
|
|
|
2084
2907
|
hint: "Pass the source app id to remix."
|
|
2085
2908
|
});
|
|
2086
2909
|
}
|
|
2087
|
-
const
|
|
2910
|
+
const sourceApp = await pollAppReady(params.api, sourceAppId);
|
|
2911
|
+
const sourceCollab = sourceApp.collab && typeof sourceApp.collab === "object" ? sourceApp.collab : null;
|
|
2912
|
+
const sourceSource = sourceApp.source && typeof sourceApp.source === "object" ? sourceApp.source : null;
|
|
2913
|
+
const sourceBranchName = typeof sourceCollab?.branchName === "string" && sourceCollab.branchName.trim() || typeof sourceSource?.branch === "string" && sourceSource.branch.trim() || typeof sourceSource?.defaultBranch === "string" && sourceSource.defaultBranch.trim() || void 0;
|
|
2914
|
+
const forkResp = await params.api.forkApp(sourceAppId, {
|
|
2915
|
+
name: params.name?.trim() || void 0,
|
|
2916
|
+
platform: "generic",
|
|
2917
|
+
branchName: sourceBranchName
|
|
2918
|
+
});
|
|
2088
2919
|
const forked = unwrapResponseObject(forkResp, "fork");
|
|
2089
2920
|
const app = await pollAppReady(params.api, String(forked.id));
|
|
2921
|
+
const collab = app.collab && typeof app.collab === "object" ? app.collab : null;
|
|
2922
|
+
const source = app.source && typeof app.source === "object" ? app.source : null;
|
|
2923
|
+
const authoritativeBranchName = typeof collab?.branchName === "string" && collab.branchName.trim() || typeof source?.branch === "string" && source.branch.trim() || typeof source?.defaultBranch === "string" && source.defaultBranch.trim() || sourceBranchName || null;
|
|
2924
|
+
const authoritativeRemoteUrl = typeof source?.remoteUrl === "string" && source.remoteUrl.trim() || null;
|
|
2925
|
+
const authoritativeDefaultBranch = typeof source?.defaultBranch === "string" && source.defaultBranch.trim() || null;
|
|
2090
2926
|
const checkout = await materializeAppCheckout({
|
|
2091
2927
|
api: params.api,
|
|
2092
2928
|
cwd: params.cwd,
|
|
2093
2929
|
appId: String(app.id),
|
|
2094
2930
|
outputDir: params.outputDir ?? null,
|
|
2095
|
-
defaultDirName: sanitizeCheckoutDirName(String(params.name?.trim() || app.name || app.id))
|
|
2931
|
+
defaultDirName: sanitizeCheckoutDirName(String(params.name?.trim() || app.name || app.id)),
|
|
2932
|
+
expectedBranchName: authoritativeBranchName,
|
|
2933
|
+
expectedRemoteUrl: authoritativeRemoteUrl,
|
|
2934
|
+
expectedDefaultBranch: authoritativeDefaultBranch
|
|
2096
2935
|
});
|
|
2097
|
-
const
|
|
2936
|
+
const laneId = typeof collab?.laneId === "string" ? collab.laneId : null;
|
|
2937
|
+
const repoFingerprint = typeof source?.repoFingerprint === "string" ? source.repoFingerprint : checkout.repoFingerprint;
|
|
2938
|
+
const remoteUrl = typeof source?.remoteUrl === "string" ? source.remoteUrl : checkout.remoteUrl;
|
|
2939
|
+
const defaultBranch = typeof source?.defaultBranch === "string" ? source.defaultBranch : checkout.defaultBranch;
|
|
2940
|
+
const branchName = authoritativeBranchName ?? checkout.branchName;
|
|
2941
|
+
const authoritativeLane = await resolveProjectLaneIfAuthoritative(params.api, {
|
|
2098
2942
|
projectId: String(app.projectId),
|
|
2943
|
+
repoFingerprint,
|
|
2944
|
+
remoteUrl,
|
|
2945
|
+
defaultBranch,
|
|
2946
|
+
branchName,
|
|
2099
2947
|
currentAppId: String(app.id),
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2948
|
+
expectedUpstreamAppId: String(app.forkedFromAppId ?? sourceAppId),
|
|
2949
|
+
createIfMissing: true,
|
|
2950
|
+
seedAppId: String(app.id)
|
|
2951
|
+
});
|
|
2952
|
+
const bindingPath = await writeCollabBinding(checkout.repoRoot, {
|
|
2953
|
+
projectId: authoritativeLane?.projectId ?? String(app.projectId),
|
|
2954
|
+
currentAppId: authoritativeLane?.currentAppId ?? String(app.id),
|
|
2955
|
+
upstreamAppId: authoritativeLane?.upstreamAppId ?? String(app.forkedFromAppId ?? sourceAppId),
|
|
2956
|
+
threadId: authoritativeLane?.threadId ?? (app.threadId ? String(app.threadId) : null),
|
|
2957
|
+
repoFingerprint: authoritativeLane?.repoFingerprint ?? repoFingerprint,
|
|
2958
|
+
remoteUrl: authoritativeLane?.remoteUrl ?? remoteUrl,
|
|
2959
|
+
defaultBranch: authoritativeLane?.defaultBranch ?? defaultBranch,
|
|
2960
|
+
laneId: authoritativeLane?.laneId ?? laneId,
|
|
2961
|
+
branchName: authoritativeLane?.branchName ?? branchName,
|
|
2962
|
+
bindingMode: "lane"
|
|
2106
2963
|
});
|
|
2107
2964
|
return {
|
|
2108
|
-
appId: String(app.id),
|
|
2109
|
-
dashboardUrl: buildDashboardAppUrl(String(app.id)),
|
|
2110
|
-
projectId: String(app.projectId),
|
|
2111
|
-
upstreamAppId: String(app.forkedFromAppId ?? sourceAppId),
|
|
2965
|
+
appId: authoritativeLane?.currentAppId ?? String(app.id),
|
|
2966
|
+
dashboardUrl: buildDashboardAppUrl(authoritativeLane?.currentAppId ?? String(app.id)),
|
|
2967
|
+
projectId: authoritativeLane?.projectId ?? String(app.projectId),
|
|
2968
|
+
upstreamAppId: authoritativeLane?.upstreamAppId ?? String(app.forkedFromAppId ?? sourceAppId),
|
|
2112
2969
|
bindingPath,
|
|
2113
2970
|
repoRoot: checkout.repoRoot
|
|
2114
2971
|
};
|
|
@@ -2117,7 +2974,11 @@ async function collabRemix(params) {
|
|
|
2117
2974
|
// src/application/collab/collabRequestMerge.ts
|
|
2118
2975
|
async function collabRequestMerge(params) {
|
|
2119
2976
|
const repoRoot = await findGitRoot(params.cwd);
|
|
2120
|
-
const binding = await
|
|
2977
|
+
const binding = await ensureActiveLaneBinding({
|
|
2978
|
+
repoRoot,
|
|
2979
|
+
api: params.api,
|
|
2980
|
+
operation: "`remix collab request-merge`"
|
|
2981
|
+
});
|
|
2121
2982
|
if (!binding) throw new RemixError("Repository is not bound to Remix.", { exitCode: 2 });
|
|
2122
2983
|
const resp = await params.api.openMergeRequest(binding.currentAppId);
|
|
2123
2984
|
return unwrapResponseObject(resp, "merge request");
|
|
@@ -2152,7 +3013,9 @@ function createBaseStatus() {
|
|
|
2152
3013
|
repoFingerprint: null,
|
|
2153
3014
|
remoteUrl: null,
|
|
2154
3015
|
defaultBranch: null,
|
|
2155
|
-
|
|
3016
|
+
laneId: null,
|
|
3017
|
+
branchName: null,
|
|
3018
|
+
bindingMode: null
|
|
2156
3019
|
},
|
|
2157
3020
|
remote: {
|
|
2158
3021
|
checked: false,
|
|
@@ -2216,12 +3079,13 @@ async function collabStatus(params) {
|
|
|
2216
3079
|
}
|
|
2217
3080
|
status.repo.isGitRepo = true;
|
|
2218
3081
|
status.repo.repoRoot = repoRoot;
|
|
2219
|
-
const [branch,
|
|
3082
|
+
const [branch, initialHeadCommitHash, worktreeStatus, bindingResolution] = await Promise.all([
|
|
2220
3083
|
getCurrentBranch(repoRoot),
|
|
2221
3084
|
getHeadCommitHash(repoRoot),
|
|
2222
3085
|
getWorktreeStatus(repoRoot),
|
|
2223
|
-
|
|
3086
|
+
resolveActiveLaneBinding({ repoRoot, api: params.api ?? void 0 })
|
|
2224
3087
|
]);
|
|
3088
|
+
let headCommitHash = initialHeadCommitHash;
|
|
2225
3089
|
status.repo.branch = branch;
|
|
2226
3090
|
status.repo.branchMismatch = false;
|
|
2227
3091
|
status.repo.headCommitHash = headCommitHash;
|
|
@@ -2235,13 +3099,61 @@ async function collabStatus(params) {
|
|
|
2235
3099
|
if (!status.repo.worktree.isClean) addWarning(status, "Working tree has local changes.");
|
|
2236
3100
|
if (!branch) addWarning(status, "Repository is in a detached HEAD state.");
|
|
2237
3101
|
if (!headCommitHash) addWarning(status, "Failed to resolve local HEAD commit.");
|
|
2238
|
-
if (
|
|
3102
|
+
if (bindingResolution.status === "not_bound") {
|
|
2239
3103
|
status.binding.path = null;
|
|
2240
3104
|
addBlockedReason(status.sync, "not_bound");
|
|
2241
3105
|
addBlockedReason(status.reconcile, "not_bound");
|
|
2242
3106
|
status.recommendedAction = "init";
|
|
2243
3107
|
return status;
|
|
2244
3108
|
}
|
|
3109
|
+
if (bindingResolution.status === "missing_branch_binding") {
|
|
3110
|
+
status.binding = {
|
|
3111
|
+
isBound: true,
|
|
3112
|
+
path: getCollabBindingPath(repoRoot),
|
|
3113
|
+
projectId: bindingResolution.projectId,
|
|
3114
|
+
currentAppId: null,
|
|
3115
|
+
upstreamAppId: bindingResolution.upstreamAppId,
|
|
3116
|
+
isRemix: null,
|
|
3117
|
+
threadId: bindingResolution.threadId,
|
|
3118
|
+
repoFingerprint: bindingResolution.repoFingerprint,
|
|
3119
|
+
remoteUrl: bindingResolution.remoteUrl,
|
|
3120
|
+
defaultBranch: bindingResolution.defaultBranch,
|
|
3121
|
+
laneId: null,
|
|
3122
|
+
branchName: bindingResolution.currentBranch,
|
|
3123
|
+
bindingMode: "lane"
|
|
3124
|
+
};
|
|
3125
|
+
addBlockedReason(status.sync, "branch_binding_missing");
|
|
3126
|
+
addBlockedReason(status.reconcile, "branch_binding_missing");
|
|
3127
|
+
addWarning(status, `Current branch ${bindingResolution.currentBranch ?? "(detached)"} is not yet bound to a Remix lane.`);
|
|
3128
|
+
status.recommendedAction = "init";
|
|
3129
|
+
return status;
|
|
3130
|
+
}
|
|
3131
|
+
if (bindingResolution.status === "ambiguous_family_selection") {
|
|
3132
|
+
status.binding = {
|
|
3133
|
+
isBound: true,
|
|
3134
|
+
path: getCollabBindingPath(repoRoot),
|
|
3135
|
+
projectId: null,
|
|
3136
|
+
currentAppId: null,
|
|
3137
|
+
upstreamAppId: null,
|
|
3138
|
+
isRemix: null,
|
|
3139
|
+
threadId: null,
|
|
3140
|
+
repoFingerprint: bindingResolution.repoFingerprint,
|
|
3141
|
+
remoteUrl: bindingResolution.remoteUrl,
|
|
3142
|
+
defaultBranch: bindingResolution.defaultBranch,
|
|
3143
|
+
laneId: null,
|
|
3144
|
+
branchName: bindingResolution.currentBranch,
|
|
3145
|
+
bindingMode: null
|
|
3146
|
+
};
|
|
3147
|
+
addBlockedReason(status.sync, "family_ambiguous");
|
|
3148
|
+
addBlockedReason(status.reconcile, "family_ambiguous");
|
|
3149
|
+
addWarning(
|
|
3150
|
+
status,
|
|
3151
|
+
`Multiple canonical Remix families match ${bindingResolution.currentBranch ?? "the current branch"}. Switch to a checkout already bound to the intended family or run \`remix collab init --force-new\`.`
|
|
3152
|
+
);
|
|
3153
|
+
status.recommendedAction = "choose_family";
|
|
3154
|
+
return status;
|
|
3155
|
+
}
|
|
3156
|
+
const binding = bindingResolution.binding;
|
|
2245
3157
|
status.binding = {
|
|
2246
3158
|
isBound: true,
|
|
2247
3159
|
path: getCollabBindingPath(repoRoot),
|
|
@@ -2253,13 +3165,21 @@ async function collabStatus(params) {
|
|
|
2253
3165
|
repoFingerprint: binding.repoFingerprint,
|
|
2254
3166
|
remoteUrl: binding.remoteUrl,
|
|
2255
3167
|
defaultBranch: binding.defaultBranch,
|
|
2256
|
-
|
|
3168
|
+
laneId: binding.laneId,
|
|
3169
|
+
branchName: binding.branchName,
|
|
3170
|
+
bindingMode: binding.bindingMode
|
|
2257
3171
|
};
|
|
2258
|
-
status.repo.branchMismatch = !
|
|
3172
|
+
status.repo.branchMismatch = !isBoundBranchMatch(branch, binding.branchName);
|
|
2259
3173
|
if (status.repo.branchMismatch) {
|
|
2260
3174
|
addWarning(
|
|
2261
3175
|
status,
|
|
2262
|
-
`Current branch ${branch ?? "(detached)"} does not match
|
|
3176
|
+
`Current branch ${branch ?? "(detached)"} does not match bound branch ${binding.branchName ?? "(unset)"}.`
|
|
3177
|
+
);
|
|
3178
|
+
}
|
|
3179
|
+
if (bindingResolution.status === "binding_conflict") {
|
|
3180
|
+
addWarning(
|
|
3181
|
+
status,
|
|
3182
|
+
`Local binding app ${binding.currentAppId} conflicts with server-resolved app ${bindingResolution.resolvedLane.currentAppId ?? "(unknown)"} for branch ${bindingResolution.currentBranch ?? "(detached)"}.`
|
|
2263
3183
|
);
|
|
2264
3184
|
}
|
|
2265
3185
|
if (!params.api) {
|
|
@@ -2307,8 +3227,14 @@ async function collabStatus(params) {
|
|
|
2307
3227
|
const syncResp = syncResult.value;
|
|
2308
3228
|
if (syncResp) {
|
|
2309
3229
|
const sync = unwrapResponseObject(syncResp, "sync result");
|
|
3230
|
+
const latestHeadCommitHash = sync.targetCommitHash && sync.status !== "up_to_date" ? await getHeadCommitHash(repoRoot) : headCommitHash;
|
|
3231
|
+
if (latestHeadCommitHash) {
|
|
3232
|
+
headCommitHash = latestHeadCommitHash;
|
|
3233
|
+
status.repo.headCommitHash = latestHeadCommitHash;
|
|
3234
|
+
}
|
|
3235
|
+
const normalizedSyncStatus = headCommitHash && sync.targetCommitHash && headCommitHash === sync.targetCommitHash ? "up_to_date" : sync.status;
|
|
2310
3236
|
status.sync.checked = true;
|
|
2311
|
-
status.sync.status =
|
|
3237
|
+
status.sync.status = normalizedSyncStatus;
|
|
2312
3238
|
status.sync.warnings = sync.warnings;
|
|
2313
3239
|
status.sync.targetCommitHash = sync.targetCommitHash;
|
|
2314
3240
|
status.sync.targetCommitId = sync.targetCommitId;
|
|
@@ -2316,9 +3242,9 @@ async function collabStatus(params) {
|
|
|
2316
3242
|
if (!status.repo.worktree.isClean) addBlockedReason(status.sync, "dirty_worktree");
|
|
2317
3243
|
if (!branch) addBlockedReason(status.sync, "detached_head");
|
|
2318
3244
|
if (status.repo.branchMismatch) addBlockedReason(status.sync, "branch_mismatch");
|
|
2319
|
-
if (
|
|
3245
|
+
if (normalizedSyncStatus === "conflict_risk") addBlockedReason(status.sync, "metadata_conflict");
|
|
2320
3246
|
status.sync.canApply = status.sync.status === "ready_to_fast_forward" && status.repo.worktree.isClean && Boolean(branch) && !status.sync.blockedReasons.includes("metadata_conflict");
|
|
2321
|
-
if (
|
|
3247
|
+
if (normalizedSyncStatus === "conflict_risk") {
|
|
2322
3248
|
status.reconcile.checked = true;
|
|
2323
3249
|
status.reconcile.status = "metadata_conflict";
|
|
2324
3250
|
status.reconcile.warnings = sync.warnings;
|
|
@@ -2328,7 +3254,7 @@ async function collabStatus(params) {
|
|
|
2328
3254
|
if (!status.repo.worktree.isClean) addBlockedReason(status.reconcile, "dirty_worktree");
|
|
2329
3255
|
if (!branch) addBlockedReason(status.reconcile, "detached_head");
|
|
2330
3256
|
if (status.repo.branchMismatch) addBlockedReason(status.reconcile, "branch_mismatch");
|
|
2331
|
-
} else if (
|
|
3257
|
+
} else if (normalizedSyncStatus === "base_unknown") {
|
|
2332
3258
|
try {
|
|
2333
3259
|
const preflightResp = await params.api.preflightAppReconcile(binding.currentAppId, {
|
|
2334
3260
|
localHeadCommitHash: headCommitHash,
|
|
@@ -2376,6 +3302,8 @@ async function collabStatus(params) {
|
|
|
2376
3302
|
status.recommendedAction = "reconcile";
|
|
2377
3303
|
} else if ((status.remote.incomingOpenMergeRequestCount ?? 0) > 0) {
|
|
2378
3304
|
status.recommendedAction = "review_queue";
|
|
3305
|
+
} else if (status.sync.blockedReasons.includes("family_ambiguous") || status.reconcile.blockedReasons.includes("family_ambiguous")) {
|
|
3306
|
+
status.recommendedAction = "choose_family";
|
|
2379
3307
|
} else {
|
|
2380
3308
|
status.recommendedAction = "no_action";
|
|
2381
3309
|
}
|
|
@@ -2385,7 +3313,11 @@ async function collabStatus(params) {
|
|
|
2385
3313
|
// src/application/collab/collabSyncUpstream.ts
|
|
2386
3314
|
async function collabSyncUpstream(params) {
|
|
2387
3315
|
const repoRoot = await findGitRoot(params.cwd);
|
|
2388
|
-
const binding = await
|
|
3316
|
+
const binding = await ensureActiveLaneBinding({
|
|
3317
|
+
repoRoot,
|
|
3318
|
+
api: params.api,
|
|
3319
|
+
operation: "`remix collab sync-upstream`"
|
|
3320
|
+
});
|
|
2389
3321
|
if (!binding) {
|
|
2390
3322
|
throw new RemixError("Repository is not bound to Remix.", {
|
|
2391
3323
|
exitCode: 2,
|