@remixhq/core 0.1.10 → 0.1.11
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-K54U353Z.js +691 -0
- package/dist/chunk-RM2BGDBB.js +400 -0
- package/dist/chunk-ZXP6ENQY.js +244 -0
- package/dist/collab.d.ts +44 -6
- package/dist/collab.js +740 -129
- 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,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getCollabBindingPath,
|
|
3
3
|
readCollabBinding,
|
|
4
|
+
readCollabBindingState,
|
|
4
5
|
reserveAvailableDirPath,
|
|
5
6
|
reserveDirectory,
|
|
6
7
|
writeCollabBinding
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-4L3ZBZUQ.js";
|
|
9
|
+
import "./chunk-HZNEDSRS.js";
|
|
8
10
|
import {
|
|
9
11
|
assertRepoSnapshotUnchanged,
|
|
10
12
|
buildRepoFingerprint,
|
|
@@ -38,7 +40,7 @@ import {
|
|
|
38
40
|
summarizeUnifiedDiff,
|
|
39
41
|
validateUnifiedDiff,
|
|
40
42
|
writeTempUnifiedDiffBackup
|
|
41
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-RREREIGW.js";
|
|
42
44
|
import {
|
|
43
45
|
REMIX_ERROR_CODES
|
|
44
46
|
} from "./chunk-GC2MOT3U.js";
|
|
@@ -55,29 +57,29 @@ function describeBranch(value) {
|
|
|
55
57
|
const normalized = String(value ?? "").trim();
|
|
56
58
|
return normalized || "(detached)";
|
|
57
59
|
}
|
|
58
|
-
function
|
|
60
|
+
function isBoundBranchMatch(currentBranch, branchName) {
|
|
59
61
|
const current = String(currentBranch ?? "").trim();
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
62
|
-
return current ===
|
|
62
|
+
const expected = String(branchName ?? "").trim();
|
|
63
|
+
if (!expected || !current) return true;
|
|
64
|
+
return current === expected;
|
|
63
65
|
}
|
|
64
|
-
function
|
|
66
|
+
function buildBranchMismatchHint(params) {
|
|
65
67
|
const overrideFlag = params.overrideFlag?.trim() || "--allow-branch-mismatch";
|
|
66
68
|
return [
|
|
67
69
|
`Current branch: ${describeBranch(params.currentBranch)}`,
|
|
68
|
-
`
|
|
69
|
-
`Switch to ${describeBranch(params.
|
|
70
|
+
`Bound branch: ${describeBranch(params.branchName)}`,
|
|
71
|
+
`Switch to ${describeBranch(params.branchName)} or rerun with ${overrideFlag} if this is intentional.`
|
|
70
72
|
].join("\n");
|
|
71
73
|
}
|
|
72
|
-
function
|
|
74
|
+
function assertBoundBranchMatch(params) {
|
|
73
75
|
if (params.allowBranchMismatch) return;
|
|
74
|
-
if (
|
|
75
|
-
throw new RemixError(`Current branch does not match this checkout's Remix
|
|
76
|
+
if (isBoundBranchMatch(params.currentBranch, params.branchName)) return;
|
|
77
|
+
throw new RemixError(`Current branch does not match this checkout's bound Remix branch while running ${params.operation}.`, {
|
|
76
78
|
code: REMIX_ERROR_CODES.PREFERRED_BRANCH_MISMATCH,
|
|
77
79
|
exitCode: 2,
|
|
78
|
-
hint:
|
|
80
|
+
hint: buildBranchMismatchHint({
|
|
79
81
|
currentBranch: params.currentBranch,
|
|
80
|
-
|
|
82
|
+
branchName: params.branchName,
|
|
81
83
|
overrideFlag: params.overrideFlag
|
|
82
84
|
})
|
|
83
85
|
});
|
|
@@ -141,6 +143,52 @@ function sanitizeCheckoutDirName(value) {
|
|
|
141
143
|
function buildDashboardAppUrl(appId) {
|
|
142
144
|
return `https://dashboard.remix.one/apps/${encodeURIComponent(appId)}`;
|
|
143
145
|
}
|
|
146
|
+
async function resolveProjectLaneIfAuthoritative(api, params) {
|
|
147
|
+
const branchName = String(params.branchName ?? "").trim();
|
|
148
|
+
if (!branchName) return null;
|
|
149
|
+
if (!params.projectId && !params.repoFingerprint && !params.remoteUrl) return null;
|
|
150
|
+
const readLane = async () => {
|
|
151
|
+
const laneResp = await api.resolveProjectLaneBinding({
|
|
152
|
+
projectId: params.projectId ?? void 0,
|
|
153
|
+
repoFingerprint: params.repoFingerprint ?? void 0,
|
|
154
|
+
remoteUrl: params.remoteUrl ?? void 0,
|
|
155
|
+
defaultBranch: params.defaultBranch ?? void 0,
|
|
156
|
+
branchName
|
|
157
|
+
});
|
|
158
|
+
return unwrapResponseObject(laneResp, "project lane binding");
|
|
159
|
+
};
|
|
160
|
+
let lane = await readLane();
|
|
161
|
+
const shouldRepairResolvedLane = lane.status === "resolved" && Boolean(
|
|
162
|
+
params.expectedUpstreamAppId && (!lane.upstreamAppId || lane.upstreamAppId !== params.expectedUpstreamAppId)
|
|
163
|
+
);
|
|
164
|
+
if (shouldRepairResolvedLane && params.createIfMissing && params.seedAppId) {
|
|
165
|
+
const ensuredResp = await api.ensureProjectLaneBinding({
|
|
166
|
+
projectId: params.projectId ?? void 0,
|
|
167
|
+
repoFingerprint: params.repoFingerprint ?? void 0,
|
|
168
|
+
remoteUrl: params.remoteUrl ?? void 0,
|
|
169
|
+
defaultBranch: params.defaultBranch ?? void 0,
|
|
170
|
+
branchName,
|
|
171
|
+
seedAppId: params.seedAppId
|
|
172
|
+
});
|
|
173
|
+
lane = unwrapResponseObject(ensuredResp, "project lane binding");
|
|
174
|
+
}
|
|
175
|
+
if (lane.status !== "resolved" && params.createIfMissing && params.seedAppId) {
|
|
176
|
+
const ensuredResp = await api.ensureProjectLaneBinding({
|
|
177
|
+
projectId: params.projectId ?? void 0,
|
|
178
|
+
repoFingerprint: params.repoFingerprint ?? void 0,
|
|
179
|
+
remoteUrl: params.remoteUrl ?? void 0,
|
|
180
|
+
defaultBranch: params.defaultBranch ?? void 0,
|
|
181
|
+
branchName,
|
|
182
|
+
seedAppId: params.seedAppId
|
|
183
|
+
});
|
|
184
|
+
lane = unwrapResponseObject(ensuredResp, "project lane binding");
|
|
185
|
+
}
|
|
186
|
+
if (lane.status !== "resolved") return null;
|
|
187
|
+
if (params.projectId && lane.projectId && lane.projectId !== params.projectId) return null;
|
|
188
|
+
if (params.currentAppId && lane.currentAppId && lane.currentAppId !== params.currentAppId) return null;
|
|
189
|
+
if (params.expectedUpstreamAppId && lane.upstreamAppId && lane.upstreamAppId !== params.expectedUpstreamAppId) return null;
|
|
190
|
+
return lane;
|
|
191
|
+
}
|
|
144
192
|
async function pollAppReady(api, appId) {
|
|
145
193
|
const started = Date.now();
|
|
146
194
|
let delay = 2e3;
|
|
@@ -285,6 +333,275 @@ async function pollMergeRequestCompletion(api, mrId, params) {
|
|
|
285
333
|
throw new RemixError("Timed out waiting for merge approval to complete.", { exitCode: 1 });
|
|
286
334
|
}
|
|
287
335
|
|
|
336
|
+
// src/application/collab/laneBindingResolver.ts
|
|
337
|
+
function normalizeBranchName(value) {
|
|
338
|
+
const normalized = String(value ?? "").trim();
|
|
339
|
+
return normalized || null;
|
|
340
|
+
}
|
|
341
|
+
function buildBindingFromLane(state, lane) {
|
|
342
|
+
if (!lane.currentAppId || !lane.upstreamAppId) return null;
|
|
343
|
+
return {
|
|
344
|
+
schemaVersion: 3,
|
|
345
|
+
projectId: lane.projectId ?? state.projectId,
|
|
346
|
+
currentAppId: lane.currentAppId,
|
|
347
|
+
upstreamAppId: lane.upstreamAppId,
|
|
348
|
+
threadId: lane.threadId ?? null,
|
|
349
|
+
repoFingerprint: lane.repoFingerprint ?? state.repoFingerprint ?? null,
|
|
350
|
+
remoteUrl: lane.remoteUrl ?? state.remoteUrl ?? null,
|
|
351
|
+
defaultBranch: lane.defaultBranch ?? state.defaultBranch ?? null,
|
|
352
|
+
laneId: lane.laneId ?? null,
|
|
353
|
+
branchName: lane.branchName ?? state.currentBranch ?? null,
|
|
354
|
+
bindingMode: "lane"
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function shouldPersistRemoteLaneMetadata(localBinding, lane) {
|
|
358
|
+
return Boolean(
|
|
359
|
+
!localBinding.laneId && lane.laneId || !localBinding.threadId && lane.threadId || !localBinding.repoFingerprint && lane.repoFingerprint || !localBinding.remoteUrl && lane.remoteUrl || !localBinding.defaultBranch && lane.defaultBranch || !localBinding.branchName && lane.branchName
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
function shouldRequireRemoteLaneForCurrentBranch(params) {
|
|
363
|
+
if (!params.currentBranch) return false;
|
|
364
|
+
const defaultBranch = normalizeBranchName(params.defaultBranch);
|
|
365
|
+
if (params.currentBranch === defaultBranch) return false;
|
|
366
|
+
return !params.binding.laneId || params.binding.currentAppId === params.binding.upstreamAppId;
|
|
367
|
+
}
|
|
368
|
+
async function persistResolvedLane(repoRoot, binding) {
|
|
369
|
+
await writeCollabBinding(repoRoot, {
|
|
370
|
+
projectId: binding.projectId,
|
|
371
|
+
currentAppId: binding.currentAppId,
|
|
372
|
+
upstreamAppId: binding.upstreamAppId,
|
|
373
|
+
threadId: binding.threadId,
|
|
374
|
+
repoFingerprint: binding.repoFingerprint,
|
|
375
|
+
remoteUrl: binding.remoteUrl,
|
|
376
|
+
defaultBranch: binding.defaultBranch,
|
|
377
|
+
laneId: binding.laneId,
|
|
378
|
+
branchName: binding.branchName,
|
|
379
|
+
bindingMode: binding.bindingMode
|
|
380
|
+
});
|
|
381
|
+
return readCollabBinding(repoRoot);
|
|
382
|
+
}
|
|
383
|
+
async function resolveActiveLaneBinding(params) {
|
|
384
|
+
const state = await readCollabBindingState(params.repoRoot);
|
|
385
|
+
if (!state) {
|
|
386
|
+
return { status: "not_bound", currentBranch: null };
|
|
387
|
+
}
|
|
388
|
+
const currentBranch = normalizeBranchName(state.currentBranch);
|
|
389
|
+
const localBinding = state.binding;
|
|
390
|
+
if (localBinding) {
|
|
391
|
+
const requireRemoteLane = shouldRequireRemoteLaneForCurrentBranch({
|
|
392
|
+
binding: localBinding,
|
|
393
|
+
currentBranch,
|
|
394
|
+
defaultBranch: state.defaultBranch
|
|
395
|
+
});
|
|
396
|
+
if (!params.api || !currentBranch) {
|
|
397
|
+
return {
|
|
398
|
+
status: "resolved",
|
|
399
|
+
source: "local",
|
|
400
|
+
binding: localBinding,
|
|
401
|
+
currentBranch
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
const laneResp2 = await params.api.resolveProjectLaneBinding({
|
|
405
|
+
projectId: localBinding.projectId ?? state.projectId ?? void 0,
|
|
406
|
+
repoFingerprint: state.repoFingerprint ?? void 0,
|
|
407
|
+
remoteUrl: state.remoteUrl ?? void 0,
|
|
408
|
+
defaultBranch: state.defaultBranch ?? void 0,
|
|
409
|
+
branchName: currentBranch
|
|
410
|
+
});
|
|
411
|
+
const lane2 = unwrapResponseObject(laneResp2, "project lane binding");
|
|
412
|
+
if (lane2.status === "resolved") {
|
|
413
|
+
const resolvedBranch = normalizeBranchName(lane2.branchName);
|
|
414
|
+
const resolvedProjectId = lane2.projectId ?? state.projectId;
|
|
415
|
+
const branchConflict = Boolean(resolvedBranch && localBinding.branchName && resolvedBranch !== localBinding.branchName);
|
|
416
|
+
const appConflict = Boolean(lane2.currentAppId && lane2.currentAppId !== localBinding.currentAppId);
|
|
417
|
+
const upstreamConflict = Boolean(lane2.upstreamAppId && lane2.upstreamAppId !== localBinding.upstreamAppId);
|
|
418
|
+
const projectConflict = Boolean(resolvedProjectId && localBinding.projectId && resolvedProjectId !== localBinding.projectId);
|
|
419
|
+
if (branchConflict || appConflict || upstreamConflict || projectConflict) {
|
|
420
|
+
if (requireRemoteLane) {
|
|
421
|
+
const binding = buildBindingFromLane(state, lane2);
|
|
422
|
+
if (binding) {
|
|
423
|
+
return {
|
|
424
|
+
status: "resolved",
|
|
425
|
+
source: "remote",
|
|
426
|
+
binding,
|
|
427
|
+
currentBranch
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
status: "binding_conflict",
|
|
433
|
+
binding: localBinding,
|
|
434
|
+
resolvedLane: lane2,
|
|
435
|
+
currentBranch
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
if (shouldPersistRemoteLaneMetadata(localBinding, lane2)) {
|
|
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
|
+
}
|
|
450
|
+
if (requireRemoteLane) {
|
|
451
|
+
return {
|
|
452
|
+
status: "missing_branch_binding",
|
|
453
|
+
currentBranch,
|
|
454
|
+
projectId: state.projectId,
|
|
455
|
+
repoFingerprint: state.repoFingerprint,
|
|
456
|
+
remoteUrl: state.remoteUrl,
|
|
457
|
+
defaultBranch: state.defaultBranch,
|
|
458
|
+
upstreamAppId: localBinding.upstreamAppId ?? null,
|
|
459
|
+
threadId: localBinding.threadId ?? null
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
status: "resolved",
|
|
464
|
+
source: "local",
|
|
465
|
+
binding: localBinding,
|
|
466
|
+
currentBranch
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
if (!params.api || !currentBranch) {
|
|
470
|
+
return {
|
|
471
|
+
status: "missing_branch_binding",
|
|
472
|
+
currentBranch,
|
|
473
|
+
projectId: state.projectId,
|
|
474
|
+
repoFingerprint: state.repoFingerprint,
|
|
475
|
+
remoteUrl: state.remoteUrl,
|
|
476
|
+
defaultBranch: state.defaultBranch,
|
|
477
|
+
upstreamAppId: null,
|
|
478
|
+
threadId: null
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
const laneResp = await params.api.resolveProjectLaneBinding({
|
|
482
|
+
projectId: state.projectId ?? void 0,
|
|
483
|
+
repoFingerprint: state.repoFingerprint ?? void 0,
|
|
484
|
+
remoteUrl: state.remoteUrl ?? void 0,
|
|
485
|
+
defaultBranch: state.defaultBranch ?? void 0,
|
|
486
|
+
branchName: currentBranch
|
|
487
|
+
});
|
|
488
|
+
const lane = unwrapResponseObject(laneResp, "project lane binding");
|
|
489
|
+
if (lane.status === "resolved") {
|
|
490
|
+
const binding = buildBindingFromLane(state, lane);
|
|
491
|
+
if (binding) {
|
|
492
|
+
return {
|
|
493
|
+
status: "resolved",
|
|
494
|
+
source: "remote",
|
|
495
|
+
binding,
|
|
496
|
+
currentBranch
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (lane.status === "binding_not_found") {
|
|
501
|
+
return { status: "not_bound", currentBranch };
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
status: "missing_branch_binding",
|
|
505
|
+
currentBranch,
|
|
506
|
+
projectId: lane.projectId ?? state.projectId,
|
|
507
|
+
repoFingerprint: lane.repoFingerprint ?? state.repoFingerprint,
|
|
508
|
+
remoteUrl: lane.remoteUrl ?? state.remoteUrl,
|
|
509
|
+
defaultBranch: lane.defaultBranch ?? state.defaultBranch,
|
|
510
|
+
upstreamAppId: lane.upstreamAppId ?? null,
|
|
511
|
+
threadId: lane.threadId ?? null
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
async function ensureActiveLaneBinding(params) {
|
|
515
|
+
const resolved = await resolveActiveLaneBinding({
|
|
516
|
+
repoRoot: params.repoRoot,
|
|
517
|
+
api: params.api
|
|
518
|
+
});
|
|
519
|
+
if (resolved.status === "resolved") {
|
|
520
|
+
if (resolved.source === "local") {
|
|
521
|
+
return resolved.binding;
|
|
522
|
+
}
|
|
523
|
+
return persistResolvedLane(params.repoRoot, resolved.binding);
|
|
524
|
+
}
|
|
525
|
+
if (resolved.status === "binding_conflict") {
|
|
526
|
+
throw new RemixError("Current branch binding conflicts with the server-resolved Remix lane.", {
|
|
527
|
+
exitCode: 2,
|
|
528
|
+
hint: `Local app ${resolved.binding.currentAppId}; server app ${resolved.resolvedLane.currentAppId ?? "(unknown)"}. Repair the branch binding before running ${params.operation ?? "this command"}.`
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
if (resolved.status === "not_bound") {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
if (!resolved.currentBranch) {
|
|
535
|
+
throw new RemixError("Current branch is not yet bound to a Remix lane.", {
|
|
536
|
+
exitCode: 2,
|
|
537
|
+
hint: `Switch to a named branch before running ${params.operation ?? "this command"}.`
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
throw new RemixError("Current branch is not yet bound to a Remix lane.", {
|
|
541
|
+
exitCode: 2,
|
|
542
|
+
hint: `Run \`remix collab init\` on branch ${resolved.currentBranch} before running ${params.operation ?? "this command"}.`
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
async function provisionActiveLaneBinding(params) {
|
|
546
|
+
const resolved = await resolveActiveLaneBinding(params);
|
|
547
|
+
if (resolved.status === "resolved") {
|
|
548
|
+
if (resolved.source === "local") {
|
|
549
|
+
return { binding: resolved.binding, warnings: [] };
|
|
550
|
+
}
|
|
551
|
+
const persisted2 = await persistResolvedLane(params.repoRoot, resolved.binding);
|
|
552
|
+
return { binding: persisted2, warnings: [] };
|
|
553
|
+
}
|
|
554
|
+
if (resolved.status === "binding_conflict") {
|
|
555
|
+
throw new RemixError("Current branch binding conflicts with the server-resolved Remix lane.", {
|
|
556
|
+
exitCode: 2,
|
|
557
|
+
hint: `Local app ${resolved.binding.currentAppId}; server app ${resolved.resolvedLane.currentAppId ?? "(unknown)"}. Repair the branch binding before running ${params.operation}.`
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
if (resolved.status === "not_bound") {
|
|
561
|
+
return { binding: null, warnings: [] };
|
|
562
|
+
}
|
|
563
|
+
if (!resolved.currentBranch) {
|
|
564
|
+
throw new RemixError("Current branch is not yet bound to a Remix lane.", {
|
|
565
|
+
exitCode: 2,
|
|
566
|
+
hint: `Switch to a named branch before running ${params.operation}.`
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
let lane;
|
|
570
|
+
try {
|
|
571
|
+
const laneResp = await params.api.ensureProjectLaneBinding({
|
|
572
|
+
projectId: resolved.projectId ?? void 0,
|
|
573
|
+
repoFingerprint: resolved.repoFingerprint ?? void 0,
|
|
574
|
+
remoteUrl: resolved.remoteUrl ?? void 0,
|
|
575
|
+
defaultBranch: resolved.defaultBranch ?? void 0,
|
|
576
|
+
branchName: resolved.currentBranch
|
|
577
|
+
});
|
|
578
|
+
lane = unwrapResponseObject(laneResp, "project lane binding");
|
|
579
|
+
} catch (error) {
|
|
580
|
+
throw new RemixError(`Failed to provision a Remix lane for branch ${resolved.currentBranch}.`, {
|
|
581
|
+
exitCode: error instanceof RemixError ? error.exitCode : 1,
|
|
582
|
+
hint: formatCliErrorDetail(error) || `Remix could not create or recover the branch lane required before ${params.operation}.`
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
const state = await readCollabBindingState(params.repoRoot);
|
|
586
|
+
if (!state) {
|
|
587
|
+
return { binding: null, warnings: [] };
|
|
588
|
+
}
|
|
589
|
+
const binding = buildBindingFromLane(state, lane);
|
|
590
|
+
if (!binding) {
|
|
591
|
+
throw new RemixError(`Failed to provision a Remix lane for branch ${resolved.currentBranch}.`, {
|
|
592
|
+
exitCode: 1,
|
|
593
|
+
hint: "The server returned incomplete lane binding metadata."
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
const persisted = await persistResolvedLane(params.repoRoot, binding);
|
|
597
|
+
return {
|
|
598
|
+
binding: persisted,
|
|
599
|
+
warnings: [
|
|
600
|
+
lane.created ? `Provisioned Remix lane for branch ${resolved.currentBranch}.` : `Recovered existing Remix lane binding for branch ${resolved.currentBranch}.`
|
|
601
|
+
]
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
288
605
|
// src/application/collab/recordingPreflight.ts
|
|
289
606
|
async function collabRecordingPreflight(params) {
|
|
290
607
|
let repoRoot;
|
|
@@ -297,7 +614,7 @@ async function collabRecordingPreflight(params) {
|
|
|
297
614
|
repoRoot: null,
|
|
298
615
|
appId: null,
|
|
299
616
|
currentBranch: null,
|
|
300
|
-
|
|
617
|
+
branchName: null,
|
|
301
618
|
headCommitHash: null,
|
|
302
619
|
worktreeClean: false,
|
|
303
620
|
syncStatus: null,
|
|
@@ -309,14 +626,14 @@ async function collabRecordingPreflight(params) {
|
|
|
309
626
|
hint: message
|
|
310
627
|
};
|
|
311
628
|
}
|
|
312
|
-
const
|
|
313
|
-
if (
|
|
629
|
+
const bindingResolution = await resolveActiveLaneBinding({ repoRoot, api: params.api });
|
|
630
|
+
if (bindingResolution.status === "not_bound") {
|
|
314
631
|
return {
|
|
315
632
|
status: "not_bound",
|
|
316
633
|
repoRoot,
|
|
317
634
|
appId: null,
|
|
318
635
|
currentBranch: null,
|
|
319
|
-
|
|
636
|
+
branchName: null,
|
|
320
637
|
headCommitHash: null,
|
|
321
638
|
worktreeClean: false,
|
|
322
639
|
syncStatus: null,
|
|
@@ -328,19 +645,56 @@ async function collabRecordingPreflight(params) {
|
|
|
328
645
|
hint: "Run `remix collab init` first."
|
|
329
646
|
};
|
|
330
647
|
}
|
|
648
|
+
if (bindingResolution.status === "missing_branch_binding") {
|
|
649
|
+
return {
|
|
650
|
+
status: "branch_binding_missing",
|
|
651
|
+
repoRoot,
|
|
652
|
+
appId: null,
|
|
653
|
+
currentBranch: bindingResolution.currentBranch,
|
|
654
|
+
branchName: bindingResolution.currentBranch,
|
|
655
|
+
headCommitHash: null,
|
|
656
|
+
worktreeClean: false,
|
|
657
|
+
syncStatus: null,
|
|
658
|
+
syncTargetCommitHash: null,
|
|
659
|
+
syncTargetCommitId: null,
|
|
660
|
+
reconcileTargetHeadCommitHash: null,
|
|
661
|
+
reconcileTargetHeadCommitId: null,
|
|
662
|
+
warnings: [],
|
|
663
|
+
hint: `Current branch ${bindingResolution.currentBranch ?? "(detached)"} is not yet bound to a Remix lane.`
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
if (bindingResolution.status === "binding_conflict") {
|
|
667
|
+
return {
|
|
668
|
+
status: "metadata_conflict",
|
|
669
|
+
repoRoot,
|
|
670
|
+
appId: bindingResolution.binding.currentAppId,
|
|
671
|
+
currentBranch: bindingResolution.currentBranch,
|
|
672
|
+
branchName: bindingResolution.binding.branchName,
|
|
673
|
+
headCommitHash: null,
|
|
674
|
+
worktreeClean: false,
|
|
675
|
+
syncStatus: null,
|
|
676
|
+
syncTargetCommitHash: null,
|
|
677
|
+
syncTargetCommitId: null,
|
|
678
|
+
reconcileTargetHeadCommitHash: null,
|
|
679
|
+
reconcileTargetHeadCommitId: null,
|
|
680
|
+
warnings: [],
|
|
681
|
+
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.`
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
const binding = bindingResolution.binding;
|
|
331
685
|
const [currentBranch, headCommitHash, worktreeStatus] = await Promise.all([
|
|
332
686
|
getCurrentBranch(repoRoot),
|
|
333
687
|
getHeadCommitHash(repoRoot),
|
|
334
688
|
getWorktreeStatus(repoRoot)
|
|
335
689
|
]);
|
|
336
|
-
const
|
|
690
|
+
const branchName = binding.branchName ?? null;
|
|
337
691
|
if (!headCommitHash) {
|
|
338
692
|
return {
|
|
339
693
|
status: "missing_head",
|
|
340
694
|
repoRoot,
|
|
341
695
|
appId: binding.currentAppId,
|
|
342
696
|
currentBranch,
|
|
343
|
-
|
|
697
|
+
branchName,
|
|
344
698
|
headCommitHash: null,
|
|
345
699
|
worktreeClean: worktreeStatus.isClean,
|
|
346
700
|
syncStatus: null,
|
|
@@ -352,13 +706,13 @@ async function collabRecordingPreflight(params) {
|
|
|
352
706
|
hint: "Failed to resolve local HEAD commit."
|
|
353
707
|
};
|
|
354
708
|
}
|
|
355
|
-
if (!params.allowBranchMismatch && !
|
|
709
|
+
if (!params.allowBranchMismatch && !isBoundBranchMatch(currentBranch, branchName)) {
|
|
356
710
|
return {
|
|
357
711
|
status: "branch_mismatch",
|
|
358
712
|
repoRoot,
|
|
359
713
|
appId: binding.currentAppId,
|
|
360
714
|
currentBranch,
|
|
361
|
-
|
|
715
|
+
branchName,
|
|
362
716
|
headCommitHash,
|
|
363
717
|
worktreeClean: worktreeStatus.isClean,
|
|
364
718
|
syncStatus: null,
|
|
@@ -367,9 +721,9 @@ async function collabRecordingPreflight(params) {
|
|
|
367
721
|
reconcileTargetHeadCommitHash: null,
|
|
368
722
|
reconcileTargetHeadCommitId: null,
|
|
369
723
|
warnings: [],
|
|
370
|
-
hint:
|
|
724
|
+
hint: buildBranchMismatchHint({
|
|
371
725
|
currentBranch,
|
|
372
|
-
|
|
726
|
+
branchName
|
|
373
727
|
})
|
|
374
728
|
};
|
|
375
729
|
}
|
|
@@ -387,7 +741,7 @@ async function collabRecordingPreflight(params) {
|
|
|
387
741
|
repoRoot,
|
|
388
742
|
appId: binding.currentAppId,
|
|
389
743
|
currentBranch,
|
|
390
|
-
|
|
744
|
+
branchName,
|
|
391
745
|
headCommitHash,
|
|
392
746
|
worktreeClean: worktreeStatus.isClean,
|
|
393
747
|
syncStatus: sync.status,
|
|
@@ -405,7 +759,7 @@ async function collabRecordingPreflight(params) {
|
|
|
405
759
|
repoRoot,
|
|
406
760
|
appId: binding.currentAppId,
|
|
407
761
|
currentBranch,
|
|
408
|
-
|
|
762
|
+
branchName,
|
|
409
763
|
headCommitHash,
|
|
410
764
|
worktreeClean: worktreeStatus.isClean,
|
|
411
765
|
syncStatus: sync.status,
|
|
@@ -430,7 +784,7 @@ async function collabRecordingPreflight(params) {
|
|
|
430
784
|
repoRoot,
|
|
431
785
|
appId: binding.currentAppId,
|
|
432
786
|
currentBranch,
|
|
433
|
-
|
|
787
|
+
branchName,
|
|
434
788
|
headCommitHash,
|
|
435
789
|
worktreeClean: worktreeStatus.isClean,
|
|
436
790
|
syncStatus: sync.status,
|
|
@@ -448,7 +802,7 @@ async function collabRecordingPreflight(params) {
|
|
|
448
802
|
repoRoot,
|
|
449
803
|
appId: binding.currentAppId,
|
|
450
804
|
currentBranch,
|
|
451
|
-
|
|
805
|
+
branchName,
|
|
452
806
|
headCommitHash,
|
|
453
807
|
worktreeClean: worktreeStatus.isClean,
|
|
454
808
|
syncStatus: sync.status,
|
|
@@ -465,7 +819,7 @@ async function collabRecordingPreflight(params) {
|
|
|
465
819
|
repoRoot,
|
|
466
820
|
appId: binding.currentAppId,
|
|
467
821
|
currentBranch,
|
|
468
|
-
|
|
822
|
+
branchName,
|
|
469
823
|
headCommitHash,
|
|
470
824
|
worktreeClean: worktreeStatus.isClean,
|
|
471
825
|
syncStatus: sync.status,
|
|
@@ -697,7 +1051,11 @@ import os2 from "os";
|
|
|
697
1051
|
import path2 from "path";
|
|
698
1052
|
async function collabSync(params) {
|
|
699
1053
|
const repoRoot = await findGitRoot(params.cwd);
|
|
700
|
-
const binding = await
|
|
1054
|
+
const binding = await ensureActiveLaneBinding({
|
|
1055
|
+
repoRoot,
|
|
1056
|
+
api: params.api,
|
|
1057
|
+
operation: "`remix collab sync`"
|
|
1058
|
+
});
|
|
701
1059
|
if (!binding) {
|
|
702
1060
|
throw new RemixError("Repository is not bound to Remix.", {
|
|
703
1061
|
exitCode: 2,
|
|
@@ -706,9 +1064,9 @@ async function collabSync(params) {
|
|
|
706
1064
|
}
|
|
707
1065
|
await ensureCleanWorktree(repoRoot);
|
|
708
1066
|
const branch = await requireCurrentBranch(repoRoot);
|
|
709
|
-
|
|
1067
|
+
assertBoundBranchMatch({
|
|
710
1068
|
currentBranch: branch,
|
|
711
|
-
|
|
1069
|
+
branchName: binding.branchName,
|
|
712
1070
|
allowBranchMismatch: params.allowBranchMismatch,
|
|
713
1071
|
operation: "`remix collab sync`"
|
|
714
1072
|
});
|
|
@@ -785,9 +1143,9 @@ async function collabSync(params) {
|
|
|
785
1143
|
});
|
|
786
1144
|
await ensureCleanWorktree(lockedRepoRoot);
|
|
787
1145
|
const lockedBranch = await requireCurrentBranch(lockedRepoRoot);
|
|
788
|
-
|
|
1146
|
+
assertBoundBranchMatch({
|
|
789
1147
|
currentBranch: lockedBranch,
|
|
790
|
-
|
|
1148
|
+
branchName: binding.branchName,
|
|
791
1149
|
allowBranchMismatch: params.allowBranchMismatch,
|
|
792
1150
|
operation: "`remix collab sync`"
|
|
793
1151
|
});
|
|
@@ -820,6 +1178,12 @@ function assertSupportedRecordingPreflight(preflight) {
|
|
|
820
1178
|
hint: preflight.hint
|
|
821
1179
|
});
|
|
822
1180
|
}
|
|
1181
|
+
if (preflight.status === "branch_binding_missing") {
|
|
1182
|
+
throw new RemixError("Current branch is not yet bound to a Remix lane.", {
|
|
1183
|
+
exitCode: 2,
|
|
1184
|
+
hint: preflight.hint
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
823
1187
|
if (preflight.status === "not_git_repo") {
|
|
824
1188
|
throw new RemixError(preflight.hint || "Not inside a git repository.", {
|
|
825
1189
|
exitCode: 2,
|
|
@@ -833,9 +1197,9 @@ function assertSupportedRecordingPreflight(preflight) {
|
|
|
833
1197
|
});
|
|
834
1198
|
}
|
|
835
1199
|
if (preflight.status === "branch_mismatch") {
|
|
836
|
-
|
|
1200
|
+
assertBoundBranchMatch({
|
|
837
1201
|
currentBranch: preflight.currentBranch,
|
|
838
|
-
|
|
1202
|
+
branchName: preflight.branchName,
|
|
839
1203
|
allowBranchMismatch: false,
|
|
840
1204
|
operation: "`remix collab add`"
|
|
841
1205
|
});
|
|
@@ -855,7 +1219,11 @@ function assertSupportedRecordingPreflight(preflight) {
|
|
|
855
1219
|
}
|
|
856
1220
|
async function collabAdd(params) {
|
|
857
1221
|
const repoRoot = await findGitRoot(params.cwd);
|
|
858
|
-
const binding = await
|
|
1222
|
+
const binding = await ensureActiveLaneBinding({
|
|
1223
|
+
repoRoot,
|
|
1224
|
+
api: params.api,
|
|
1225
|
+
operation: "`remix collab add`"
|
|
1226
|
+
});
|
|
859
1227
|
if (!binding) {
|
|
860
1228
|
throw new RemixError("Repository is not bound to Remix.", {
|
|
861
1229
|
exitCode: 2,
|
|
@@ -876,9 +1244,9 @@ async function collabAdd(params) {
|
|
|
876
1244
|
});
|
|
877
1245
|
assertSupportedRecordingPreflight(preflight);
|
|
878
1246
|
const branch = preflight.currentBranch;
|
|
879
|
-
|
|
1247
|
+
assertBoundBranchMatch({
|
|
880
1248
|
currentBranch: branch,
|
|
881
|
-
|
|
1249
|
+
branchName: binding.branchName,
|
|
882
1250
|
allowBranchMismatch: params.allowBranchMismatch,
|
|
883
1251
|
operation: "`remix collab add`"
|
|
884
1252
|
});
|
|
@@ -1051,6 +1419,7 @@ async function collabAdd(params) {
|
|
|
1051
1419
|
});
|
|
1052
1420
|
const resp = await params.api.createChangeStep(binding.currentAppId, {
|
|
1053
1421
|
threadId: binding.threadId ?? void 0,
|
|
1422
|
+
collabLaneId: binding.laneId ?? void 0,
|
|
1054
1423
|
prompt,
|
|
1055
1424
|
assistantResponse: assistantResponse ?? void 0,
|
|
1056
1425
|
diff,
|
|
@@ -1126,6 +1495,12 @@ function assertSupportedRecordingPreflight2(preflight) {
|
|
|
1126
1495
|
hint: preflight.hint
|
|
1127
1496
|
});
|
|
1128
1497
|
}
|
|
1498
|
+
if (preflight.status === "branch_binding_missing") {
|
|
1499
|
+
throw new RemixError("Current branch is not yet bound to a Remix lane.", {
|
|
1500
|
+
exitCode: 2,
|
|
1501
|
+
hint: preflight.hint
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1129
1504
|
if (preflight.status === "not_git_repo") {
|
|
1130
1505
|
throw new RemixError(preflight.hint || "Not inside a git repository.", {
|
|
1131
1506
|
exitCode: 2,
|
|
@@ -1139,9 +1514,9 @@ function assertSupportedRecordingPreflight2(preflight) {
|
|
|
1139
1514
|
});
|
|
1140
1515
|
}
|
|
1141
1516
|
if (preflight.status === "branch_mismatch") {
|
|
1142
|
-
|
|
1517
|
+
assertBoundBranchMatch({
|
|
1143
1518
|
currentBranch: preflight.currentBranch,
|
|
1144
|
-
|
|
1519
|
+
branchName: preflight.branchName,
|
|
1145
1520
|
allowBranchMismatch: false,
|
|
1146
1521
|
operation: "`remix collab record-turn`"
|
|
1147
1522
|
});
|
|
@@ -1161,7 +1536,11 @@ function assertSupportedRecordingPreflight2(preflight) {
|
|
|
1161
1536
|
}
|
|
1162
1537
|
async function collabRecordTurn(params) {
|
|
1163
1538
|
const repoRoot = await findGitRoot(params.cwd);
|
|
1164
|
-
const binding = await
|
|
1539
|
+
const binding = await ensureActiveLaneBinding({
|
|
1540
|
+
repoRoot,
|
|
1541
|
+
api: params.api,
|
|
1542
|
+
operation: "`remix collab record-turn`"
|
|
1543
|
+
});
|
|
1165
1544
|
if (!binding) {
|
|
1166
1545
|
throw new RemixError("Repository is not bound to Remix.", {
|
|
1167
1546
|
exitCode: 2,
|
|
@@ -1193,9 +1572,9 @@ async function collabRecordTurn(params) {
|
|
|
1193
1572
|
});
|
|
1194
1573
|
}
|
|
1195
1574
|
const branch = await getCurrentBranch(repoRoot);
|
|
1196
|
-
|
|
1575
|
+
assertBoundBranchMatch({
|
|
1197
1576
|
currentBranch: branch,
|
|
1198
|
-
|
|
1577
|
+
branchName: binding.branchName,
|
|
1199
1578
|
allowBranchMismatch: params.allowBranchMismatch,
|
|
1200
1579
|
operation: "`remix collab record-turn`"
|
|
1201
1580
|
});
|
|
@@ -1209,6 +1588,7 @@ async function collabRecordTurn(params) {
|
|
|
1209
1588
|
});
|
|
1210
1589
|
const resp = await params.api.createCollabTurn(binding.currentAppId, {
|
|
1211
1590
|
threadId: binding.threadId ?? void 0,
|
|
1591
|
+
collabLaneId: binding.laneId ?? void 0,
|
|
1212
1592
|
prompt,
|
|
1213
1593
|
assistantResponse,
|
|
1214
1594
|
actor: params.actor,
|
|
@@ -1221,7 +1601,8 @@ async function collabRecordTurn(params) {
|
|
|
1221
1601
|
},
|
|
1222
1602
|
idempotencyKey
|
|
1223
1603
|
});
|
|
1224
|
-
|
|
1604
|
+
const turn = unwrapResponseObject(resp, "collab turn");
|
|
1605
|
+
return turn;
|
|
1225
1606
|
}
|
|
1226
1607
|
|
|
1227
1608
|
// src/application/collab/collabFinalizeTurn.ts
|
|
@@ -1231,7 +1612,11 @@ function collectWarnings(value) {
|
|
|
1231
1612
|
}
|
|
1232
1613
|
async function collabFinalizeTurn(params) {
|
|
1233
1614
|
const repoRoot = await findGitRoot(params.cwd);
|
|
1234
|
-
const binding = await
|
|
1615
|
+
const binding = await ensureActiveLaneBinding({
|
|
1616
|
+
repoRoot,
|
|
1617
|
+
api: params.api,
|
|
1618
|
+
operation: "`remix collab finalize-turn`"
|
|
1619
|
+
});
|
|
1235
1620
|
if (!binding) {
|
|
1236
1621
|
throw new RemixError("Repository is not bound to Remix.", {
|
|
1237
1622
|
exitCode: 2,
|
|
@@ -1339,7 +1724,11 @@ async function collabApprove(params) {
|
|
|
1339
1724
|
operation: "collabApproveSyncTarget"
|
|
1340
1725
|
},
|
|
1341
1726
|
async ({ repoRoot, warnings }) => {
|
|
1342
|
-
const binding = await
|
|
1727
|
+
const binding = await ensureActiveLaneBinding({
|
|
1728
|
+
repoRoot,
|
|
1729
|
+
api: params.api,
|
|
1730
|
+
operation: "`remix collab approve --sync-target-repo`"
|
|
1731
|
+
});
|
|
1343
1732
|
if (!binding) {
|
|
1344
1733
|
throw new RemixError("Repository is not bound to Remix.", {
|
|
1345
1734
|
exitCode: 2,
|
|
@@ -1348,9 +1737,9 @@ async function collabApprove(params) {
|
|
|
1348
1737
|
}
|
|
1349
1738
|
await ensureCleanWorktree(repoRoot, "`remix collab approve --sync-target-repo`");
|
|
1350
1739
|
const branch = await requireCurrentBranch(repoRoot);
|
|
1351
|
-
|
|
1740
|
+
assertBoundBranchMatch({
|
|
1352
1741
|
currentBranch: branch,
|
|
1353
|
-
|
|
1742
|
+
branchName: binding.branchName,
|
|
1354
1743
|
allowBranchMismatch: params.allowBranchMismatch,
|
|
1355
1744
|
operation: "`remix collab approve --sync-target-repo`"
|
|
1356
1745
|
});
|
|
@@ -1433,10 +1822,6 @@ function isSubpath(parentPath, candidatePath) {
|
|
|
1433
1822
|
const relative = path4.relative(parentPath, candidatePath);
|
|
1434
1823
|
return relative === "" || !relative.startsWith("..") && !path4.isAbsolute(relative);
|
|
1435
1824
|
}
|
|
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
1825
|
async function resolveCheckoutDestination(params) {
|
|
1441
1826
|
if (params.outputDir?.trim()) {
|
|
1442
1827
|
const preferredRepoRoot = path4.resolve(params.outputDir.trim());
|
|
@@ -1501,7 +1886,9 @@ async function materializeAppCheckout(params) {
|
|
|
1501
1886
|
const bundle = await params.api.downloadAppBundle(params.appId);
|
|
1502
1887
|
await fs4.writeFile(bundlePath, bundle.data);
|
|
1503
1888
|
await cloneGitBundleToDirectory(bundlePath, repoRoot);
|
|
1504
|
-
|
|
1889
|
+
if (params.expectedBranchName?.trim()) {
|
|
1890
|
+
await checkoutLocalBranch(repoRoot, params.expectedBranchName.trim());
|
|
1891
|
+
}
|
|
1505
1892
|
await ensureGitInfoExcludeEntries(repoRoot, [".remix/"]);
|
|
1506
1893
|
} catch (err) {
|
|
1507
1894
|
await fs4.rm(repoRoot, { recursive: true, force: true }).catch(() => {
|
|
@@ -1510,15 +1897,15 @@ async function materializeAppCheckout(params) {
|
|
|
1510
1897
|
} finally {
|
|
1511
1898
|
await fs4.rm(bundleTempDir, { recursive: true, force: true });
|
|
1512
1899
|
}
|
|
1513
|
-
const
|
|
1514
|
-
const
|
|
1515
|
-
const
|
|
1900
|
+
const branchName = await getCurrentBranch(repoRoot) ?? params.expectedBranchName?.trim() ?? null;
|
|
1901
|
+
const remoteUrl = normalizeGitRemote(params.expectedRemoteUrl ?? await getRemoteOriginUrl(repoRoot));
|
|
1902
|
+
const defaultBranch = params.expectedDefaultBranch?.trim() ?? await getDefaultBranch(repoRoot) ?? branchName ?? null;
|
|
1516
1903
|
const repoFingerprint = remoteUrl ? await buildRepoFingerprint({ gitRoot: repoRoot, remoteUrl, defaultBranch }) : null;
|
|
1517
1904
|
return {
|
|
1518
1905
|
repoRoot,
|
|
1519
1906
|
remoteUrl,
|
|
1520
1907
|
defaultBranch,
|
|
1521
|
-
|
|
1908
|
+
branchName,
|
|
1522
1909
|
repoFingerprint
|
|
1523
1910
|
};
|
|
1524
1911
|
}
|
|
@@ -1533,29 +1920,55 @@ async function collabCheckout(params) {
|
|
|
1533
1920
|
});
|
|
1534
1921
|
}
|
|
1535
1922
|
const app = await pollAppReady(params.api, appId);
|
|
1923
|
+
const collab = app.collab && typeof app.collab === "object" ? app.collab : null;
|
|
1924
|
+
const source = app.source && typeof app.source === "object" ? app.source : null;
|
|
1925
|
+
const authoritativeBranchName = typeof collab?.branchName === "string" && collab.branchName.trim() || typeof source?.branch === "string" && source.branch.trim() || typeof source?.defaultBranch === "string" && source.defaultBranch.trim() || null;
|
|
1926
|
+
const authoritativeRemoteUrl = typeof source?.remoteUrl === "string" && source.remoteUrl.trim() || null;
|
|
1927
|
+
const authoritativeDefaultBranch = typeof source?.defaultBranch === "string" && source.defaultBranch.trim() || null;
|
|
1536
1928
|
const checkout = await materializeAppCheckout({
|
|
1537
1929
|
api: params.api,
|
|
1538
1930
|
cwd: params.cwd,
|
|
1539
1931
|
appId: String(app.id),
|
|
1540
1932
|
outputDir: params.outputDir ?? null,
|
|
1541
|
-
defaultDirName: sanitizeCheckoutDirName(String(app.name || app.id))
|
|
1933
|
+
defaultDirName: sanitizeCheckoutDirName(String(app.name || app.id)),
|
|
1934
|
+
expectedBranchName: authoritativeBranchName,
|
|
1935
|
+
expectedRemoteUrl: authoritativeRemoteUrl,
|
|
1936
|
+
expectedDefaultBranch: authoritativeDefaultBranch
|
|
1542
1937
|
});
|
|
1543
1938
|
const upstreamAppId = String(app.forkedFromAppId ?? app.id);
|
|
1544
|
-
const
|
|
1939
|
+
const laneId = typeof collab?.laneId === "string" ? collab.laneId : null;
|
|
1940
|
+
const repoFingerprint = typeof source?.repoFingerprint === "string" ? source.repoFingerprint : checkout.repoFingerprint;
|
|
1941
|
+
const remoteUrl = typeof source?.remoteUrl === "string" ? source.remoteUrl : checkout.remoteUrl;
|
|
1942
|
+
const defaultBranch = typeof source?.defaultBranch === "string" ? source.defaultBranch : checkout.defaultBranch;
|
|
1943
|
+
const branchName = authoritativeBranchName ?? checkout.branchName;
|
|
1944
|
+
const authoritativeLane = await resolveProjectLaneIfAuthoritative(params.api, {
|
|
1545
1945
|
projectId: String(app.projectId),
|
|
1946
|
+
repoFingerprint,
|
|
1947
|
+
remoteUrl,
|
|
1948
|
+
defaultBranch,
|
|
1949
|
+
branchName,
|
|
1546
1950
|
currentAppId: String(app.id),
|
|
1547
|
-
upstreamAppId,
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1951
|
+
expectedUpstreamAppId: upstreamAppId,
|
|
1952
|
+
createIfMissing: true,
|
|
1953
|
+
seedAppId: String(app.id)
|
|
1954
|
+
});
|
|
1955
|
+
const bindingPath = await writeCollabBinding(checkout.repoRoot, {
|
|
1956
|
+
projectId: authoritativeLane?.projectId ?? String(app.projectId),
|
|
1957
|
+
currentAppId: authoritativeLane?.currentAppId ?? String(app.id),
|
|
1958
|
+
upstreamAppId: authoritativeLane?.upstreamAppId ?? upstreamAppId,
|
|
1959
|
+
threadId: authoritativeLane?.threadId ?? (app.threadId ? String(app.threadId) : null),
|
|
1960
|
+
repoFingerprint: authoritativeLane?.repoFingerprint ?? repoFingerprint,
|
|
1961
|
+
remoteUrl: authoritativeLane?.remoteUrl ?? remoteUrl,
|
|
1962
|
+
defaultBranch: authoritativeLane?.defaultBranch ?? defaultBranch,
|
|
1963
|
+
laneId: authoritativeLane?.laneId ?? laneId,
|
|
1964
|
+
branchName: authoritativeLane?.branchName ?? branchName,
|
|
1965
|
+
bindingMode: "lane"
|
|
1553
1966
|
});
|
|
1554
1967
|
return {
|
|
1555
|
-
appId: String(app.id),
|
|
1556
|
-
dashboardUrl: buildDashboardAppUrl(String(app.id)),
|
|
1557
|
-
projectId: String(app.projectId),
|
|
1558
|
-
upstreamAppId,
|
|
1968
|
+
appId: authoritativeLane?.currentAppId ?? String(app.id),
|
|
1969
|
+
dashboardUrl: buildDashboardAppUrl(authoritativeLane?.currentAppId ?? String(app.id)),
|
|
1970
|
+
projectId: authoritativeLane?.projectId ?? String(app.projectId),
|
|
1971
|
+
upstreamAppId: authoritativeLane?.upstreamAppId ?? upstreamAppId,
|
|
1559
1972
|
bindingPath,
|
|
1560
1973
|
repoRoot: checkout.repoRoot
|
|
1561
1974
|
};
|
|
@@ -1577,14 +1990,26 @@ async function resolveQueueAppId(params) {
|
|
|
1577
1990
|
});
|
|
1578
1991
|
}
|
|
1579
1992
|
const repoRoot = await findGitRoot(params.cwd);
|
|
1580
|
-
const
|
|
1581
|
-
if (
|
|
1993
|
+
const bindingResolution = await resolveActiveLaneBinding({ repoRoot });
|
|
1994
|
+
if (bindingResolution.status === "not_bound") {
|
|
1582
1995
|
throw new RemixError("Repository is not bound to Remix.", {
|
|
1583
1996
|
exitCode: 2,
|
|
1584
1997
|
hint: "Bind the repository first or pass `appId` explicitly for the app-scoped merge-request queue."
|
|
1585
1998
|
});
|
|
1586
1999
|
}
|
|
1587
|
-
|
|
2000
|
+
if (bindingResolution.status === "missing_branch_binding") {
|
|
2001
|
+
throw new RemixError("Current branch is not yet bound to a Remix lane.", {
|
|
2002
|
+
exitCode: 2,
|
|
2003
|
+
hint: `Switch back to a bound branch or create a lane by recording work on ${bindingResolution.currentBranch ?? "this branch"}.`
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
if (bindingResolution.status === "binding_conflict") {
|
|
2007
|
+
throw new RemixError("Current branch binding conflicts with the server-resolved Remix lane.", {
|
|
2008
|
+
exitCode: 2,
|
|
2009
|
+
hint: `Local app ${bindingResolution.binding.currentAppId}; server app ${bindingResolution.resolvedLane.currentAppId ?? "(unknown)"}.`
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
return bindingResolution.binding.currentAppId;
|
|
1588
2013
|
}
|
|
1589
2014
|
async function collabListMergeRequests(params) {
|
|
1590
2015
|
const appId = await resolveQueueAppId({
|
|
@@ -1615,12 +2040,32 @@ async function collabListMergeRequests(params) {
|
|
|
1615
2040
|
async function resolveScopeTarget(params) {
|
|
1616
2041
|
if (params.targetId?.trim()) return params.targetId.trim();
|
|
1617
2042
|
const repoRoot = await findGitRoot(params.cwd);
|
|
1618
|
-
const
|
|
1619
|
-
if (
|
|
2043
|
+
const bindingResolution = await resolveActiveLaneBinding({ repoRoot, api: params.api });
|
|
2044
|
+
if (bindingResolution.status === "not_bound") {
|
|
1620
2045
|
throw new RemixError("Repository is not bound to Remix and no explicit target id was provided.", { exitCode: 2 });
|
|
1621
2046
|
}
|
|
1622
|
-
if (
|
|
2047
|
+
if (bindingResolution.status === "missing_branch_binding") {
|
|
2048
|
+
throw new RemixError("Current branch is not yet bound to a Remix lane and no explicit target id was provided.", {
|
|
2049
|
+
exitCode: 2
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
if (bindingResolution.status === "binding_conflict") {
|
|
2053
|
+
throw new RemixError("Current branch binding conflicts with the server-resolved Remix lane.", {
|
|
2054
|
+
exitCode: 2,
|
|
2055
|
+
hint: `Local app ${bindingResolution.binding.currentAppId}; server app ${bindingResolution.resolvedLane.currentAppId ?? "(unknown)"}.`
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
const binding = bindingResolution.binding;
|
|
2059
|
+
if (params.scope === "project") {
|
|
2060
|
+
if (!binding.projectId) {
|
|
2061
|
+
throw new RemixError("Could not resolve the project for the current repository binding.", { exitCode: 2 });
|
|
2062
|
+
}
|
|
2063
|
+
return binding.projectId;
|
|
2064
|
+
}
|
|
1623
2065
|
if (params.scope === "app") return binding.currentAppId;
|
|
2066
|
+
if (!binding.projectId) {
|
|
2067
|
+
throw new RemixError("Could not resolve the project for the current repository binding.", { exitCode: 2 });
|
|
2068
|
+
}
|
|
1624
2069
|
const project = unwrapResponseObject(await params.api.getProject(binding.projectId), "project");
|
|
1625
2070
|
const organizationId = typeof project.organizationId === "string" ? project.organizationId : null;
|
|
1626
2071
|
if (!organizationId) {
|
|
@@ -1758,13 +2203,28 @@ async function collabInit(params) {
|
|
|
1758
2203
|
const remoteUrl = normalizeGitRemote(await getRemoteOriginUrl(repoRoot));
|
|
1759
2204
|
const currentBranch = await getCurrentBranch(repoRoot);
|
|
1760
2205
|
const defaultBranch = await getDefaultBranch(repoRoot) ?? currentBranch;
|
|
1761
|
-
const
|
|
2206
|
+
const branchName = currentBranch ?? defaultBranch ?? null;
|
|
1762
2207
|
const repoFingerprint = await buildRepoFingerprint({ gitRoot: repoRoot, remoteUrl, defaultBranch });
|
|
1763
2208
|
const repoSnapshot = await captureRepoSnapshot(repoRoot);
|
|
2209
|
+
if (params.forceNew) {
|
|
2210
|
+
const bindingResp = await params.api.resolveProjectBinding({
|
|
2211
|
+
repoFingerprint,
|
|
2212
|
+
remoteUrl: remoteUrl ?? void 0,
|
|
2213
|
+
branchName: branchName ?? void 0
|
|
2214
|
+
});
|
|
2215
|
+
const existing = bindingResp?.responseObject;
|
|
2216
|
+
if (existing?.projectId && existing?.appId) {
|
|
2217
|
+
throw new RemixError("`remix collab init --force-new` is not allowed for repositories already known to Remix.", {
|
|
2218
|
+
exitCode: 2,
|
|
2219
|
+
hint: "This repository already resolves to an existing Remix lineage. Run `remix collab init` without `--force-new`, or use an explicit checkout/remix flow instead of creating a duplicate imported app."
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
1764
2223
|
if (!params.forceNew) {
|
|
1765
2224
|
const bindingResp = await params.api.resolveProjectBinding({
|
|
1766
2225
|
repoFingerprint,
|
|
1767
|
-
remoteUrl: remoteUrl ?? void 0
|
|
2226
|
+
remoteUrl: remoteUrl ?? void 0,
|
|
2227
|
+
branchName: branchName ?? void 0
|
|
1768
2228
|
});
|
|
1769
2229
|
const existing = bindingResp?.responseObject;
|
|
1770
2230
|
if (existing?.projectId && existing?.appId) {
|
|
@@ -1772,25 +2232,55 @@ async function collabInit(params) {
|
|
|
1772
2232
|
operation: "`remix collab init`",
|
|
1773
2233
|
recoveryHint: "The repository changed while the local binding was being initialized. Review the local changes and rerun `remix collab init`."
|
|
1774
2234
|
});
|
|
2235
|
+
const initialProjectId = String(existing.projectId);
|
|
2236
|
+
const initialCurrentAppId = String(existing.appId);
|
|
2237
|
+
const initialUpstreamAppId = String(existing.upstreamAppId ?? existing.appId);
|
|
2238
|
+
const initialThreadId = existing.threadId ? String(existing.threadId) : null;
|
|
1775
2239
|
const bindingPath2 = await writeCollabBinding(repoRoot, {
|
|
1776
|
-
projectId:
|
|
1777
|
-
currentAppId:
|
|
1778
|
-
upstreamAppId:
|
|
1779
|
-
threadId:
|
|
2240
|
+
projectId: initialProjectId,
|
|
2241
|
+
currentAppId: initialCurrentAppId,
|
|
2242
|
+
upstreamAppId: initialUpstreamAppId,
|
|
2243
|
+
threadId: initialThreadId,
|
|
1780
2244
|
repoFingerprint,
|
|
1781
2245
|
remoteUrl,
|
|
1782
2246
|
defaultBranch: defaultBranch ?? null,
|
|
1783
|
-
|
|
2247
|
+
laneId: null,
|
|
2248
|
+
branchName,
|
|
2249
|
+
bindingMode: "lane"
|
|
1784
2250
|
});
|
|
2251
|
+
let boundProjectId2 = initialProjectId;
|
|
2252
|
+
let boundCurrentAppId2 = initialCurrentAppId;
|
|
2253
|
+
let boundUpstreamAppId2 = initialUpstreamAppId;
|
|
2254
|
+
let boundThreadId2 = initialThreadId;
|
|
2255
|
+
let finalWarnings = [...warnings];
|
|
2256
|
+
if (branchName) {
|
|
2257
|
+
const provisioned = await provisionActiveLaneBinding({
|
|
2258
|
+
repoRoot,
|
|
2259
|
+
api: params.api,
|
|
2260
|
+
operation: "`remix collab init`"
|
|
2261
|
+
});
|
|
2262
|
+
if (provisioned.binding) {
|
|
2263
|
+
boundProjectId2 = provisioned.binding.projectId ?? boundProjectId2;
|
|
2264
|
+
boundCurrentAppId2 = provisioned.binding.currentAppId;
|
|
2265
|
+
boundUpstreamAppId2 = provisioned.binding.upstreamAppId;
|
|
2266
|
+
boundThreadId2 = provisioned.binding.threadId;
|
|
2267
|
+
}
|
|
2268
|
+
finalWarnings = [...finalWarnings, ...provisioned.warnings];
|
|
2269
|
+
}
|
|
2270
|
+
if (boundCurrentAppId2) {
|
|
2271
|
+
const readyApp = await pollAppReady(params.api, boundCurrentAppId2);
|
|
2272
|
+
boundProjectId2 = String(readyApp.projectId ?? boundProjectId2);
|
|
2273
|
+
boundThreadId2 = readyApp.threadId ? String(readyApp.threadId) : boundThreadId2;
|
|
2274
|
+
}
|
|
1785
2275
|
return {
|
|
1786
2276
|
reused: true,
|
|
1787
|
-
projectId:
|
|
1788
|
-
appId:
|
|
1789
|
-
dashboardUrl: buildDashboardAppUrl(
|
|
1790
|
-
upstreamAppId:
|
|
2277
|
+
projectId: boundProjectId2,
|
|
2278
|
+
appId: boundCurrentAppId2,
|
|
2279
|
+
dashboardUrl: buildDashboardAppUrl(boundCurrentAppId2),
|
|
2280
|
+
upstreamAppId: boundUpstreamAppId2,
|
|
1791
2281
|
bindingPath: bindingPath2,
|
|
1792
2282
|
repoRoot,
|
|
1793
|
-
...
|
|
2283
|
+
...finalWarnings.length > 0 ? { warnings: finalWarnings } : {}
|
|
1794
2284
|
};
|
|
1795
2285
|
}
|
|
1796
2286
|
}
|
|
@@ -1817,6 +2307,7 @@ async function collabInit(params) {
|
|
|
1817
2307
|
path: params.path?.trim() || void 0,
|
|
1818
2308
|
platform: "generic",
|
|
1819
2309
|
isPublic: false,
|
|
2310
|
+
branch: currentBranch ?? void 0,
|
|
1820
2311
|
remoteUrl: remoteUrl ?? void 0,
|
|
1821
2312
|
defaultBranch: defaultBranch ?? void 0,
|
|
1822
2313
|
repoFingerprint,
|
|
@@ -1824,31 +2315,65 @@ async function collabInit(params) {
|
|
|
1824
2315
|
});
|
|
1825
2316
|
const imported = unwrapResponseObject(importResp, "import");
|
|
1826
2317
|
const app = await pollAppReady(params.api, String(imported.appId));
|
|
2318
|
+
let boundProjectId = String(app.projectId);
|
|
2319
|
+
let boundCurrentAppId = String(app.id);
|
|
2320
|
+
let boundUpstreamAppId = String(app.id);
|
|
2321
|
+
let boundThreadId = app.threadId ? String(app.threadId) : null;
|
|
2322
|
+
let boundLaneId = null;
|
|
2323
|
+
if (branchName) {
|
|
2324
|
+
const laneResp = defaultBranch && branchName !== defaultBranch ? await params.api.bootstrapFreshProjectLane({
|
|
2325
|
+
projectId: boundProjectId,
|
|
2326
|
+
repoFingerprint,
|
|
2327
|
+
remoteUrl: remoteUrl ?? void 0,
|
|
2328
|
+
defaultBranch: defaultBranch ?? void 0,
|
|
2329
|
+
branchName,
|
|
2330
|
+
seedAppId: String(app.id)
|
|
2331
|
+
}) : await params.api.ensureProjectLaneBinding({
|
|
2332
|
+
projectId: boundProjectId,
|
|
2333
|
+
repoFingerprint,
|
|
2334
|
+
remoteUrl: remoteUrl ?? void 0,
|
|
2335
|
+
defaultBranch: defaultBranch ?? void 0,
|
|
2336
|
+
branchName,
|
|
2337
|
+
seedAppId: String(app.id)
|
|
2338
|
+
});
|
|
2339
|
+
const lane = unwrapResponseObject(laneResp, "project lane binding");
|
|
2340
|
+
boundProjectId = typeof lane.projectId === "string" && lane.projectId ? lane.projectId : boundProjectId;
|
|
2341
|
+
boundCurrentAppId = typeof lane.currentAppId === "string" && lane.currentAppId ? lane.currentAppId : boundCurrentAppId;
|
|
2342
|
+
boundUpstreamAppId = typeof lane.upstreamAppId === "string" && lane.upstreamAppId ? lane.upstreamAppId : boundUpstreamAppId;
|
|
2343
|
+
boundThreadId = typeof lane.threadId === "string" && lane.threadId ? lane.threadId : boundThreadId;
|
|
2344
|
+
boundLaneId = typeof lane.laneId === "string" && lane.laneId ? lane.laneId : null;
|
|
2345
|
+
}
|
|
2346
|
+
if (boundCurrentAppId) {
|
|
2347
|
+
const readyApp = await pollAppReady(params.api, boundCurrentAppId);
|
|
2348
|
+
boundProjectId = String(readyApp.projectId ?? boundProjectId);
|
|
2349
|
+
boundThreadId = readyApp.threadId ? String(readyApp.threadId) : boundThreadId;
|
|
2350
|
+
}
|
|
1827
2351
|
await assertRepoSnapshotUnchanged(repoRoot, repoSnapshot, {
|
|
1828
2352
|
operation: "`remix collab init`",
|
|
1829
2353
|
recoveryHint: "The repository changed before the Remix binding was written. Review the local changes and rerun `remix collab init`."
|
|
1830
2354
|
});
|
|
1831
2355
|
const bindingPath = await writeCollabBinding(repoRoot, {
|
|
1832
|
-
projectId:
|
|
1833
|
-
currentAppId:
|
|
1834
|
-
upstreamAppId:
|
|
1835
|
-
threadId:
|
|
2356
|
+
projectId: boundProjectId,
|
|
2357
|
+
currentAppId: boundCurrentAppId,
|
|
2358
|
+
upstreamAppId: boundUpstreamAppId,
|
|
2359
|
+
threadId: boundThreadId,
|
|
1836
2360
|
repoFingerprint,
|
|
1837
2361
|
remoteUrl,
|
|
1838
2362
|
defaultBranch: defaultBranch ?? null,
|
|
1839
|
-
|
|
2363
|
+
laneId: boundLaneId,
|
|
2364
|
+
branchName,
|
|
2365
|
+
bindingMode: "lane"
|
|
1840
2366
|
});
|
|
1841
2367
|
return {
|
|
1842
2368
|
reused: false,
|
|
1843
|
-
projectId:
|
|
1844
|
-
appId:
|
|
1845
|
-
dashboardUrl: buildDashboardAppUrl(
|
|
1846
|
-
upstreamAppId:
|
|
2369
|
+
projectId: boundProjectId,
|
|
2370
|
+
appId: boundCurrentAppId,
|
|
2371
|
+
dashboardUrl: buildDashboardAppUrl(boundCurrentAppId),
|
|
2372
|
+
upstreamAppId: boundUpstreamAppId,
|
|
1847
2373
|
bindingPath,
|
|
1848
2374
|
repoRoot,
|
|
1849
2375
|
remoteUrl,
|
|
1850
2376
|
defaultBranch,
|
|
1851
|
-
preferredBranch,
|
|
1852
2377
|
...warnings.length > 0 ? { warnings } : {}
|
|
1853
2378
|
};
|
|
1854
2379
|
}
|
|
@@ -1903,7 +2428,11 @@ import os4 from "os";
|
|
|
1903
2428
|
import path6 from "path";
|
|
1904
2429
|
async function collabReconcile(params) {
|
|
1905
2430
|
const repoRoot = await findGitRoot(params.cwd);
|
|
1906
|
-
const binding = await
|
|
2431
|
+
const binding = await ensureActiveLaneBinding({
|
|
2432
|
+
repoRoot,
|
|
2433
|
+
api: params.api,
|
|
2434
|
+
operation: "`remix collab reconcile`"
|
|
2435
|
+
});
|
|
1907
2436
|
if (!binding) {
|
|
1908
2437
|
throw new RemixError("Repository is not bound to Remix.", {
|
|
1909
2438
|
exitCode: 2,
|
|
@@ -1912,9 +2441,9 @@ async function collabReconcile(params) {
|
|
|
1912
2441
|
}
|
|
1913
2442
|
await ensureCleanWorktree(repoRoot, "`remix collab reconcile`");
|
|
1914
2443
|
const branch = await requireCurrentBranch(repoRoot);
|
|
1915
|
-
|
|
2444
|
+
assertBoundBranchMatch({
|
|
1916
2445
|
currentBranch: branch,
|
|
1917
|
-
|
|
2446
|
+
branchName: binding.branchName,
|
|
1918
2447
|
allowBranchMismatch: params.allowBranchMismatch,
|
|
1919
2448
|
operation: "`remix collab reconcile`"
|
|
1920
2449
|
});
|
|
@@ -2023,9 +2552,9 @@ async function collabReconcile(params) {
|
|
|
2023
2552
|
});
|
|
2024
2553
|
await ensureCleanWorktree(lockedRepoRoot, "`remix collab reconcile`");
|
|
2025
2554
|
const lockedBranch = await requireCurrentBranch(lockedRepoRoot);
|
|
2026
|
-
|
|
2555
|
+
assertBoundBranchMatch({
|
|
2027
2556
|
currentBranch: lockedBranch,
|
|
2028
|
-
|
|
2557
|
+
branchName: binding.branchName,
|
|
2029
2558
|
allowBranchMismatch: params.allowBranchMismatch,
|
|
2030
2559
|
operation: "`remix collab reconcile`"
|
|
2031
2560
|
});
|
|
@@ -2084,31 +2613,65 @@ async function collabRemix(params) {
|
|
|
2084
2613
|
hint: "Pass the source app id to remix."
|
|
2085
2614
|
});
|
|
2086
2615
|
}
|
|
2087
|
-
const
|
|
2616
|
+
const sourceApp = await pollAppReady(params.api, sourceAppId);
|
|
2617
|
+
const sourceCollab = sourceApp.collab && typeof sourceApp.collab === "object" ? sourceApp.collab : null;
|
|
2618
|
+
const sourceSource = sourceApp.source && typeof sourceApp.source === "object" ? sourceApp.source : null;
|
|
2619
|
+
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;
|
|
2620
|
+
const forkResp = await params.api.forkApp(sourceAppId, {
|
|
2621
|
+
name: params.name?.trim() || void 0,
|
|
2622
|
+
platform: "generic",
|
|
2623
|
+
branchName: sourceBranchName
|
|
2624
|
+
});
|
|
2088
2625
|
const forked = unwrapResponseObject(forkResp, "fork");
|
|
2089
2626
|
const app = await pollAppReady(params.api, String(forked.id));
|
|
2627
|
+
const collab = app.collab && typeof app.collab === "object" ? app.collab : null;
|
|
2628
|
+
const source = app.source && typeof app.source === "object" ? app.source : null;
|
|
2629
|
+
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;
|
|
2630
|
+
const authoritativeRemoteUrl = typeof source?.remoteUrl === "string" && source.remoteUrl.trim() || null;
|
|
2631
|
+
const authoritativeDefaultBranch = typeof source?.defaultBranch === "string" && source.defaultBranch.trim() || null;
|
|
2090
2632
|
const checkout = await materializeAppCheckout({
|
|
2091
2633
|
api: params.api,
|
|
2092
2634
|
cwd: params.cwd,
|
|
2093
2635
|
appId: String(app.id),
|
|
2094
2636
|
outputDir: params.outputDir ?? null,
|
|
2095
|
-
defaultDirName: sanitizeCheckoutDirName(String(params.name?.trim() || app.name || app.id))
|
|
2637
|
+
defaultDirName: sanitizeCheckoutDirName(String(params.name?.trim() || app.name || app.id)),
|
|
2638
|
+
expectedBranchName: authoritativeBranchName,
|
|
2639
|
+
expectedRemoteUrl: authoritativeRemoteUrl,
|
|
2640
|
+
expectedDefaultBranch: authoritativeDefaultBranch
|
|
2096
2641
|
});
|
|
2097
|
-
const
|
|
2642
|
+
const laneId = typeof collab?.laneId === "string" ? collab.laneId : null;
|
|
2643
|
+
const repoFingerprint = typeof source?.repoFingerprint === "string" ? source.repoFingerprint : checkout.repoFingerprint;
|
|
2644
|
+
const remoteUrl = typeof source?.remoteUrl === "string" ? source.remoteUrl : checkout.remoteUrl;
|
|
2645
|
+
const defaultBranch = typeof source?.defaultBranch === "string" ? source.defaultBranch : checkout.defaultBranch;
|
|
2646
|
+
const branchName = authoritativeBranchName ?? checkout.branchName;
|
|
2647
|
+
const authoritativeLane = await resolveProjectLaneIfAuthoritative(params.api, {
|
|
2098
2648
|
projectId: String(app.projectId),
|
|
2649
|
+
repoFingerprint,
|
|
2650
|
+
remoteUrl,
|
|
2651
|
+
defaultBranch,
|
|
2652
|
+
branchName,
|
|
2099
2653
|
currentAppId: String(app.id),
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2654
|
+
expectedUpstreamAppId: String(app.forkedFromAppId ?? sourceAppId),
|
|
2655
|
+
createIfMissing: true,
|
|
2656
|
+
seedAppId: String(app.id)
|
|
2657
|
+
});
|
|
2658
|
+
const bindingPath = await writeCollabBinding(checkout.repoRoot, {
|
|
2659
|
+
projectId: authoritativeLane?.projectId ?? String(app.projectId),
|
|
2660
|
+
currentAppId: authoritativeLane?.currentAppId ?? String(app.id),
|
|
2661
|
+
upstreamAppId: authoritativeLane?.upstreamAppId ?? String(app.forkedFromAppId ?? sourceAppId),
|
|
2662
|
+
threadId: authoritativeLane?.threadId ?? (app.threadId ? String(app.threadId) : null),
|
|
2663
|
+
repoFingerprint: authoritativeLane?.repoFingerprint ?? repoFingerprint,
|
|
2664
|
+
remoteUrl: authoritativeLane?.remoteUrl ?? remoteUrl,
|
|
2665
|
+
defaultBranch: authoritativeLane?.defaultBranch ?? defaultBranch,
|
|
2666
|
+
laneId: authoritativeLane?.laneId ?? laneId,
|
|
2667
|
+
branchName: authoritativeLane?.branchName ?? branchName,
|
|
2668
|
+
bindingMode: "lane"
|
|
2106
2669
|
});
|
|
2107
2670
|
return {
|
|
2108
|
-
appId: String(app.id),
|
|
2109
|
-
dashboardUrl: buildDashboardAppUrl(String(app.id)),
|
|
2110
|
-
projectId: String(app.projectId),
|
|
2111
|
-
upstreamAppId: String(app.forkedFromAppId ?? sourceAppId),
|
|
2671
|
+
appId: authoritativeLane?.currentAppId ?? String(app.id),
|
|
2672
|
+
dashboardUrl: buildDashboardAppUrl(authoritativeLane?.currentAppId ?? String(app.id)),
|
|
2673
|
+
projectId: authoritativeLane?.projectId ?? String(app.projectId),
|
|
2674
|
+
upstreamAppId: authoritativeLane?.upstreamAppId ?? String(app.forkedFromAppId ?? sourceAppId),
|
|
2112
2675
|
bindingPath,
|
|
2113
2676
|
repoRoot: checkout.repoRoot
|
|
2114
2677
|
};
|
|
@@ -2117,7 +2680,11 @@ async function collabRemix(params) {
|
|
|
2117
2680
|
// src/application/collab/collabRequestMerge.ts
|
|
2118
2681
|
async function collabRequestMerge(params) {
|
|
2119
2682
|
const repoRoot = await findGitRoot(params.cwd);
|
|
2120
|
-
const binding = await
|
|
2683
|
+
const binding = await ensureActiveLaneBinding({
|
|
2684
|
+
repoRoot,
|
|
2685
|
+
api: params.api,
|
|
2686
|
+
operation: "`remix collab request-merge`"
|
|
2687
|
+
});
|
|
2121
2688
|
if (!binding) throw new RemixError("Repository is not bound to Remix.", { exitCode: 2 });
|
|
2122
2689
|
const resp = await params.api.openMergeRequest(binding.currentAppId);
|
|
2123
2690
|
return unwrapResponseObject(resp, "merge request");
|
|
@@ -2152,7 +2719,9 @@ function createBaseStatus() {
|
|
|
2152
2719
|
repoFingerprint: null,
|
|
2153
2720
|
remoteUrl: null,
|
|
2154
2721
|
defaultBranch: null,
|
|
2155
|
-
|
|
2722
|
+
laneId: null,
|
|
2723
|
+
branchName: null,
|
|
2724
|
+
bindingMode: null
|
|
2156
2725
|
},
|
|
2157
2726
|
remote: {
|
|
2158
2727
|
checked: false,
|
|
@@ -2216,12 +2785,13 @@ async function collabStatus(params) {
|
|
|
2216
2785
|
}
|
|
2217
2786
|
status.repo.isGitRepo = true;
|
|
2218
2787
|
status.repo.repoRoot = repoRoot;
|
|
2219
|
-
const [branch,
|
|
2788
|
+
const [branch, initialHeadCommitHash, worktreeStatus, bindingResolution] = await Promise.all([
|
|
2220
2789
|
getCurrentBranch(repoRoot),
|
|
2221
2790
|
getHeadCommitHash(repoRoot),
|
|
2222
2791
|
getWorktreeStatus(repoRoot),
|
|
2223
|
-
|
|
2792
|
+
resolveActiveLaneBinding({ repoRoot, api: params.api ?? void 0 })
|
|
2224
2793
|
]);
|
|
2794
|
+
let headCommitHash = initialHeadCommitHash;
|
|
2225
2795
|
status.repo.branch = branch;
|
|
2226
2796
|
status.repo.branchMismatch = false;
|
|
2227
2797
|
status.repo.headCommitHash = headCommitHash;
|
|
@@ -2235,13 +2805,36 @@ async function collabStatus(params) {
|
|
|
2235
2805
|
if (!status.repo.worktree.isClean) addWarning(status, "Working tree has local changes.");
|
|
2236
2806
|
if (!branch) addWarning(status, "Repository is in a detached HEAD state.");
|
|
2237
2807
|
if (!headCommitHash) addWarning(status, "Failed to resolve local HEAD commit.");
|
|
2238
|
-
if (
|
|
2808
|
+
if (bindingResolution.status === "not_bound") {
|
|
2239
2809
|
status.binding.path = null;
|
|
2240
2810
|
addBlockedReason(status.sync, "not_bound");
|
|
2241
2811
|
addBlockedReason(status.reconcile, "not_bound");
|
|
2242
2812
|
status.recommendedAction = "init";
|
|
2243
2813
|
return status;
|
|
2244
2814
|
}
|
|
2815
|
+
if (bindingResolution.status === "missing_branch_binding") {
|
|
2816
|
+
status.binding = {
|
|
2817
|
+
isBound: true,
|
|
2818
|
+
path: getCollabBindingPath(repoRoot),
|
|
2819
|
+
projectId: bindingResolution.projectId,
|
|
2820
|
+
currentAppId: null,
|
|
2821
|
+
upstreamAppId: bindingResolution.upstreamAppId,
|
|
2822
|
+
isRemix: null,
|
|
2823
|
+
threadId: bindingResolution.threadId,
|
|
2824
|
+
repoFingerprint: bindingResolution.repoFingerprint,
|
|
2825
|
+
remoteUrl: bindingResolution.remoteUrl,
|
|
2826
|
+
defaultBranch: bindingResolution.defaultBranch,
|
|
2827
|
+
laneId: null,
|
|
2828
|
+
branchName: bindingResolution.currentBranch,
|
|
2829
|
+
bindingMode: "lane"
|
|
2830
|
+
};
|
|
2831
|
+
addBlockedReason(status.sync, "branch_binding_missing");
|
|
2832
|
+
addBlockedReason(status.reconcile, "branch_binding_missing");
|
|
2833
|
+
addWarning(status, `Current branch ${bindingResolution.currentBranch ?? "(detached)"} is not yet bound to a Remix lane.`);
|
|
2834
|
+
status.recommendedAction = "no_action";
|
|
2835
|
+
return status;
|
|
2836
|
+
}
|
|
2837
|
+
const binding = bindingResolution.binding;
|
|
2245
2838
|
status.binding = {
|
|
2246
2839
|
isBound: true,
|
|
2247
2840
|
path: getCollabBindingPath(repoRoot),
|
|
@@ -2253,13 +2846,21 @@ async function collabStatus(params) {
|
|
|
2253
2846
|
repoFingerprint: binding.repoFingerprint,
|
|
2254
2847
|
remoteUrl: binding.remoteUrl,
|
|
2255
2848
|
defaultBranch: binding.defaultBranch,
|
|
2256
|
-
|
|
2849
|
+
laneId: binding.laneId,
|
|
2850
|
+
branchName: binding.branchName,
|
|
2851
|
+
bindingMode: binding.bindingMode
|
|
2257
2852
|
};
|
|
2258
|
-
status.repo.branchMismatch = !
|
|
2853
|
+
status.repo.branchMismatch = !isBoundBranchMatch(branch, binding.branchName);
|
|
2259
2854
|
if (status.repo.branchMismatch) {
|
|
2260
2855
|
addWarning(
|
|
2261
2856
|
status,
|
|
2262
|
-
`Current branch ${branch ?? "(detached)"} does not match
|
|
2857
|
+
`Current branch ${branch ?? "(detached)"} does not match bound branch ${binding.branchName ?? "(unset)"}.`
|
|
2858
|
+
);
|
|
2859
|
+
}
|
|
2860
|
+
if (bindingResolution.status === "binding_conflict") {
|
|
2861
|
+
addWarning(
|
|
2862
|
+
status,
|
|
2863
|
+
`Local binding app ${binding.currentAppId} conflicts with server-resolved app ${bindingResolution.resolvedLane.currentAppId ?? "(unknown)"} for branch ${bindingResolution.currentBranch ?? "(detached)"}.`
|
|
2263
2864
|
);
|
|
2264
2865
|
}
|
|
2265
2866
|
if (!params.api) {
|
|
@@ -2307,8 +2908,14 @@ async function collabStatus(params) {
|
|
|
2307
2908
|
const syncResp = syncResult.value;
|
|
2308
2909
|
if (syncResp) {
|
|
2309
2910
|
const sync = unwrapResponseObject(syncResp, "sync result");
|
|
2911
|
+
const latestHeadCommitHash = sync.targetCommitHash && sync.status !== "up_to_date" ? await getHeadCommitHash(repoRoot) : headCommitHash;
|
|
2912
|
+
if (latestHeadCommitHash) {
|
|
2913
|
+
headCommitHash = latestHeadCommitHash;
|
|
2914
|
+
status.repo.headCommitHash = latestHeadCommitHash;
|
|
2915
|
+
}
|
|
2916
|
+
const normalizedSyncStatus = headCommitHash && sync.targetCommitHash && headCommitHash === sync.targetCommitHash ? "up_to_date" : sync.status;
|
|
2310
2917
|
status.sync.checked = true;
|
|
2311
|
-
status.sync.status =
|
|
2918
|
+
status.sync.status = normalizedSyncStatus;
|
|
2312
2919
|
status.sync.warnings = sync.warnings;
|
|
2313
2920
|
status.sync.targetCommitHash = sync.targetCommitHash;
|
|
2314
2921
|
status.sync.targetCommitId = sync.targetCommitId;
|
|
@@ -2316,9 +2923,9 @@ async function collabStatus(params) {
|
|
|
2316
2923
|
if (!status.repo.worktree.isClean) addBlockedReason(status.sync, "dirty_worktree");
|
|
2317
2924
|
if (!branch) addBlockedReason(status.sync, "detached_head");
|
|
2318
2925
|
if (status.repo.branchMismatch) addBlockedReason(status.sync, "branch_mismatch");
|
|
2319
|
-
if (
|
|
2926
|
+
if (normalizedSyncStatus === "conflict_risk") addBlockedReason(status.sync, "metadata_conflict");
|
|
2320
2927
|
status.sync.canApply = status.sync.status === "ready_to_fast_forward" && status.repo.worktree.isClean && Boolean(branch) && !status.sync.blockedReasons.includes("metadata_conflict");
|
|
2321
|
-
if (
|
|
2928
|
+
if (normalizedSyncStatus === "conflict_risk") {
|
|
2322
2929
|
status.reconcile.checked = true;
|
|
2323
2930
|
status.reconcile.status = "metadata_conflict";
|
|
2324
2931
|
status.reconcile.warnings = sync.warnings;
|
|
@@ -2328,7 +2935,7 @@ async function collabStatus(params) {
|
|
|
2328
2935
|
if (!status.repo.worktree.isClean) addBlockedReason(status.reconcile, "dirty_worktree");
|
|
2329
2936
|
if (!branch) addBlockedReason(status.reconcile, "detached_head");
|
|
2330
2937
|
if (status.repo.branchMismatch) addBlockedReason(status.reconcile, "branch_mismatch");
|
|
2331
|
-
} else if (
|
|
2938
|
+
} else if (normalizedSyncStatus === "base_unknown") {
|
|
2332
2939
|
try {
|
|
2333
2940
|
const preflightResp = await params.api.preflightAppReconcile(binding.currentAppId, {
|
|
2334
2941
|
localHeadCommitHash: headCommitHash,
|
|
@@ -2385,7 +2992,11 @@ async function collabStatus(params) {
|
|
|
2385
2992
|
// src/application/collab/collabSyncUpstream.ts
|
|
2386
2993
|
async function collabSyncUpstream(params) {
|
|
2387
2994
|
const repoRoot = await findGitRoot(params.cwd);
|
|
2388
|
-
const binding = await
|
|
2995
|
+
const binding = await ensureActiveLaneBinding({
|
|
2996
|
+
repoRoot,
|
|
2997
|
+
api: params.api,
|
|
2998
|
+
operation: "`remix collab sync-upstream`"
|
|
2999
|
+
});
|
|
2389
3000
|
if (!binding) {
|
|
2390
3001
|
throw new RemixError("Repository is not bound to Remix.", {
|
|
2391
3002
|
exitCode: 2,
|