@remixhq/claude-plugin 0.1.16 → 0.1.18
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/.claude-plugin/plugin.json +1 -1
- package/agents/remix-collab.md +9 -4
- package/dist/hook-post-collab.cjs +61 -71
- package/dist/hook-post-collab.cjs.map +1 -1
- package/dist/hook-pre-git.cjs +19 -13
- package/dist/hook-pre-git.cjs.map +1 -1
- package/dist/hook-stop-collab.cjs +1441 -1667
- package/dist/hook-stop-collab.cjs.map +1 -1
- package/dist/hook-user-prompt.cjs +33174 -153
- package/dist/hook-user-prompt.cjs.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.cjs +3177 -1597
- package/dist/mcp-server.cjs.map +1 -1
- package/package.json +3 -3
- package/skills/identity-and-scope-routing/SKILL.md +1 -0
- package/skills/review-merge-request/SKILL.md +5 -3
- package/skills/safe-collab-workflow/SKILL.md +8 -5
- package/skills/submit-change-step/SKILL.md +15 -16
|
@@ -37,8 +37,8 @@ var require_windows = __commonJS({
|
|
|
37
37
|
"use strict";
|
|
38
38
|
module2.exports = isexe;
|
|
39
39
|
isexe.sync = sync;
|
|
40
|
-
var
|
|
41
|
-
function checkPathExt(
|
|
40
|
+
var fs8 = require("fs");
|
|
41
|
+
function checkPathExt(path12, options) {
|
|
42
42
|
var pathext = options.pathExt !== void 0 ? options.pathExt : process.env.PATHEXT;
|
|
43
43
|
if (!pathext) {
|
|
44
44
|
return true;
|
|
@@ -49,25 +49,25 @@ var require_windows = __commonJS({
|
|
|
49
49
|
}
|
|
50
50
|
for (var i2 = 0; i2 < pathext.length; i2++) {
|
|
51
51
|
var p = pathext[i2].toLowerCase();
|
|
52
|
-
if (p &&
|
|
52
|
+
if (p && path12.substr(-p.length).toLowerCase() === p) {
|
|
53
53
|
return true;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
return false;
|
|
57
57
|
}
|
|
58
|
-
function checkStat(stat,
|
|
58
|
+
function checkStat(stat, path12, options) {
|
|
59
59
|
if (!stat.isSymbolicLink() && !stat.isFile()) {
|
|
60
60
|
return false;
|
|
61
61
|
}
|
|
62
|
-
return checkPathExt(
|
|
62
|
+
return checkPathExt(path12, options);
|
|
63
63
|
}
|
|
64
|
-
function isexe(
|
|
65
|
-
|
|
66
|
-
cb(er, er ? false : checkStat(stat,
|
|
64
|
+
function isexe(path12, options, cb) {
|
|
65
|
+
fs8.stat(path12, function(er, stat) {
|
|
66
|
+
cb(er, er ? false : checkStat(stat, path12, options));
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
|
-
function sync(
|
|
70
|
-
return checkStat(
|
|
69
|
+
function sync(path12, options) {
|
|
70
|
+
return checkStat(fs8.statSync(path12), path12, options);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
});
|
|
@@ -78,14 +78,14 @@ var require_mode = __commonJS({
|
|
|
78
78
|
"use strict";
|
|
79
79
|
module2.exports = isexe;
|
|
80
80
|
isexe.sync = sync;
|
|
81
|
-
var
|
|
82
|
-
function isexe(
|
|
83
|
-
|
|
81
|
+
var fs8 = require("fs");
|
|
82
|
+
function isexe(path12, options, cb) {
|
|
83
|
+
fs8.stat(path12, function(er, stat) {
|
|
84
84
|
cb(er, er ? false : checkStat(stat, options));
|
|
85
85
|
});
|
|
86
86
|
}
|
|
87
|
-
function sync(
|
|
88
|
-
return checkStat(
|
|
87
|
+
function sync(path12, options) {
|
|
88
|
+
return checkStat(fs8.statSync(path12), options);
|
|
89
89
|
}
|
|
90
90
|
function checkStat(stat, options) {
|
|
91
91
|
return stat.isFile() && checkMode(stat, options);
|
|
@@ -110,7 +110,7 @@ var require_mode = __commonJS({
|
|
|
110
110
|
var require_isexe = __commonJS({
|
|
111
111
|
"node_modules/isexe/index.js"(exports2, module2) {
|
|
112
112
|
"use strict";
|
|
113
|
-
var
|
|
113
|
+
var fs8 = require("fs");
|
|
114
114
|
var core;
|
|
115
115
|
if (process.platform === "win32" || global.TESTING_WINDOWS) {
|
|
116
116
|
core = require_windows();
|
|
@@ -119,7 +119,7 @@ var require_isexe = __commonJS({
|
|
|
119
119
|
}
|
|
120
120
|
module2.exports = isexe;
|
|
121
121
|
isexe.sync = sync;
|
|
122
|
-
function isexe(
|
|
122
|
+
function isexe(path12, options, cb) {
|
|
123
123
|
if (typeof options === "function") {
|
|
124
124
|
cb = options;
|
|
125
125
|
options = {};
|
|
@@ -129,7 +129,7 @@ var require_isexe = __commonJS({
|
|
|
129
129
|
throw new TypeError("callback not provided");
|
|
130
130
|
}
|
|
131
131
|
return new Promise(function(resolve, reject) {
|
|
132
|
-
isexe(
|
|
132
|
+
isexe(path12, options || {}, function(er, is) {
|
|
133
133
|
if (er) {
|
|
134
134
|
reject(er);
|
|
135
135
|
} else {
|
|
@@ -138,7 +138,7 @@ var require_isexe = __commonJS({
|
|
|
138
138
|
});
|
|
139
139
|
});
|
|
140
140
|
}
|
|
141
|
-
core(
|
|
141
|
+
core(path12, options || {}, function(er, is) {
|
|
142
142
|
if (er) {
|
|
143
143
|
if (er.code === "EACCES" || options && options.ignoreErrors) {
|
|
144
144
|
er = null;
|
|
@@ -148,9 +148,9 @@ var require_isexe = __commonJS({
|
|
|
148
148
|
cb(er, is);
|
|
149
149
|
});
|
|
150
150
|
}
|
|
151
|
-
function sync(
|
|
151
|
+
function sync(path12, options) {
|
|
152
152
|
try {
|
|
153
|
-
return core.sync(
|
|
153
|
+
return core.sync(path12, options || {});
|
|
154
154
|
} catch (er) {
|
|
155
155
|
if (options && options.ignoreErrors || er.code === "EACCES") {
|
|
156
156
|
return false;
|
|
@@ -167,7 +167,7 @@ var require_which = __commonJS({
|
|
|
167
167
|
"node_modules/which/which.js"(exports2, module2) {
|
|
168
168
|
"use strict";
|
|
169
169
|
var isWindows = process.platform === "win32" || process.env.OSTYPE === "cygwin" || process.env.OSTYPE === "msys";
|
|
170
|
-
var
|
|
170
|
+
var path12 = require("path");
|
|
171
171
|
var COLON = isWindows ? ";" : ":";
|
|
172
172
|
var isexe = require_isexe();
|
|
173
173
|
var getNotFoundError = (cmd) => Object.assign(new Error(`not found: ${cmd}`), { code: "ENOENT" });
|
|
@@ -205,7 +205,7 @@ var require_which = __commonJS({
|
|
|
205
205
|
return opt.all && found.length ? resolve(found) : reject(getNotFoundError(cmd));
|
|
206
206
|
const ppRaw = pathEnv[i2];
|
|
207
207
|
const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
|
|
208
|
-
const pCmd =
|
|
208
|
+
const pCmd = path12.join(pathPart, cmd);
|
|
209
209
|
const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
|
|
210
210
|
resolve(subStep(p, i2, 0));
|
|
211
211
|
});
|
|
@@ -232,7 +232,7 @@ var require_which = __commonJS({
|
|
|
232
232
|
for (let i2 = 0; i2 < pathEnv.length; i2++) {
|
|
233
233
|
const ppRaw = pathEnv[i2];
|
|
234
234
|
const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
|
|
235
|
-
const pCmd =
|
|
235
|
+
const pCmd = path12.join(pathPart, cmd);
|
|
236
236
|
const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
|
|
237
237
|
for (let j = 0; j < pathExt.length; j++) {
|
|
238
238
|
const cur = p + pathExt[j];
|
|
@@ -280,7 +280,7 @@ var require_path_key = __commonJS({
|
|
|
280
280
|
var require_resolveCommand = __commonJS({
|
|
281
281
|
"node_modules/cross-spawn/lib/util/resolveCommand.js"(exports2, module2) {
|
|
282
282
|
"use strict";
|
|
283
|
-
var
|
|
283
|
+
var path12 = require("path");
|
|
284
284
|
var which = require_which();
|
|
285
285
|
var getPathKey = require_path_key();
|
|
286
286
|
function resolveCommandAttempt(parsed, withoutPathExt) {
|
|
@@ -298,7 +298,7 @@ var require_resolveCommand = __commonJS({
|
|
|
298
298
|
try {
|
|
299
299
|
resolved = which.sync(parsed.command, {
|
|
300
300
|
path: env[getPathKey({ env })],
|
|
301
|
-
pathExt: withoutPathExt ?
|
|
301
|
+
pathExt: withoutPathExt ? path12.delimiter : void 0
|
|
302
302
|
});
|
|
303
303
|
} catch (e) {
|
|
304
304
|
} finally {
|
|
@@ -307,7 +307,7 @@ var require_resolveCommand = __commonJS({
|
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
309
|
if (resolved) {
|
|
310
|
-
resolved =
|
|
310
|
+
resolved = path12.resolve(hasCustomCwd ? parsed.options.cwd : "", resolved);
|
|
311
311
|
}
|
|
312
312
|
return resolved;
|
|
313
313
|
}
|
|
@@ -361,8 +361,8 @@ var require_shebang_command = __commonJS({
|
|
|
361
361
|
if (!match) {
|
|
362
362
|
return null;
|
|
363
363
|
}
|
|
364
|
-
const [
|
|
365
|
-
const binary =
|
|
364
|
+
const [path12, argument] = match[0].replace(/#! ?/, "").split(" ");
|
|
365
|
+
const binary = path12.split("/").pop();
|
|
366
366
|
if (binary === "env") {
|
|
367
367
|
return argument;
|
|
368
368
|
}
|
|
@@ -375,16 +375,16 @@ var require_shebang_command = __commonJS({
|
|
|
375
375
|
var require_readShebang = __commonJS({
|
|
376
376
|
"node_modules/cross-spawn/lib/util/readShebang.js"(exports2, module2) {
|
|
377
377
|
"use strict";
|
|
378
|
-
var
|
|
378
|
+
var fs8 = require("fs");
|
|
379
379
|
var shebangCommand = require_shebang_command();
|
|
380
380
|
function readShebang(command) {
|
|
381
381
|
const size = 150;
|
|
382
382
|
const buffer = Buffer.alloc(size);
|
|
383
383
|
let fd;
|
|
384
384
|
try {
|
|
385
|
-
fd =
|
|
386
|
-
|
|
387
|
-
|
|
385
|
+
fd = fs8.openSync(command, "r");
|
|
386
|
+
fs8.readSync(fd, buffer, 0, size, 0);
|
|
387
|
+
fs8.closeSync(fd);
|
|
388
388
|
} catch (e) {
|
|
389
389
|
}
|
|
390
390
|
return shebangCommand(buffer.toString());
|
|
@@ -397,7 +397,7 @@ var require_readShebang = __commonJS({
|
|
|
397
397
|
var require_parse = __commonJS({
|
|
398
398
|
"node_modules/cross-spawn/lib/parse.js"(exports2, module2) {
|
|
399
399
|
"use strict";
|
|
400
|
-
var
|
|
400
|
+
var path12 = require("path");
|
|
401
401
|
var resolveCommand = require_resolveCommand();
|
|
402
402
|
var escape = require_escape();
|
|
403
403
|
var readShebang = require_readShebang();
|
|
@@ -422,7 +422,7 @@ var require_parse = __commonJS({
|
|
|
422
422
|
const needsShell = !isExecutableRegExp.test(commandFile);
|
|
423
423
|
if (parsed.options.forceShell || needsShell) {
|
|
424
424
|
const needsDoubleEscapeMetaChars = isCmdShimRegExp.test(commandFile);
|
|
425
|
-
parsed.command =
|
|
425
|
+
parsed.command = path12.normalize(parsed.command);
|
|
426
426
|
parsed.command = escape.command(parsed.command);
|
|
427
427
|
parsed.args = parsed.args.map((arg) => escape.argument(arg, needsDoubleEscapeMetaChars));
|
|
428
428
|
const shellCommand = [parsed.command].concat(parsed.args).join(" ");
|
|
@@ -512,7 +512,7 @@ var require_cross_spawn = __commonJS({
|
|
|
512
512
|
var cp = require("child_process");
|
|
513
513
|
var parse = require_parse();
|
|
514
514
|
var enoent = require_enoent();
|
|
515
|
-
function
|
|
515
|
+
function spawn3(command, args, options) {
|
|
516
516
|
const parsed = parse(command, args, options);
|
|
517
517
|
const spawned = cp.spawn(parsed.command, parsed.args, parsed.options);
|
|
518
518
|
enoent.hookChildProcess(spawned, parsed);
|
|
@@ -524,8 +524,8 @@ var require_cross_spawn = __commonJS({
|
|
|
524
524
|
result.error = result.error || enoent.verifyENOENTSync(result.status, parsed);
|
|
525
525
|
return result;
|
|
526
526
|
}
|
|
527
|
-
module2.exports =
|
|
528
|
-
module2.exports.spawn =
|
|
527
|
+
module2.exports = spawn3;
|
|
528
|
+
module2.exports.spawn = spawn3;
|
|
529
529
|
module2.exports.sync = spawnSync2;
|
|
530
530
|
module2.exports._parse = parse;
|
|
531
531
|
module2.exports._enoent = enoent;
|
|
@@ -538,15 +538,7 @@ __export(hook_stop_collab_exports, {
|
|
|
538
538
|
runHookStopCollab: () => runHookStopCollab
|
|
539
539
|
});
|
|
540
540
|
module.exports = __toCommonJS(hook_stop_collab_exports);
|
|
541
|
-
|
|
542
|
-
// node_modules/@remixhq/core/dist/chunk-GC2MOT3U.js
|
|
543
|
-
var REMIX_ERROR_CODES = {
|
|
544
|
-
REPO_LOCK_HELD: "REPO_LOCK_HELD",
|
|
545
|
-
REPO_LOCK_TIMEOUT: "REPO_LOCK_TIMEOUT",
|
|
546
|
-
REPO_LOCK_STALE_RECOVERED: "REPO_LOCK_STALE_RECOVERED",
|
|
547
|
-
REPO_STATE_CHANGED_DURING_OPERATION: "REPO_STATE_CHANGED_DURING_OPERATION",
|
|
548
|
-
PREFERRED_BRANCH_MISMATCH: "PREFERRED_BRANCH_MISMATCH"
|
|
549
|
-
};
|
|
541
|
+
var import_node_child_process6 = require("child_process");
|
|
550
542
|
|
|
551
543
|
// node_modules/@remixhq/core/dist/chunk-YZ34ICNN.js
|
|
552
544
|
var RemixError = class extends Error {
|
|
@@ -562,12 +554,6 @@ var RemixError = class extends Error {
|
|
|
562
554
|
}
|
|
563
555
|
};
|
|
564
556
|
|
|
565
|
-
// node_modules/@remixhq/core/dist/chunk-RREREIGW.js
|
|
566
|
-
var import_promises12 = __toESM(require("fs/promises"), 1);
|
|
567
|
-
var import_crypto = require("crypto");
|
|
568
|
-
var import_os = __toESM(require("os"), 1);
|
|
569
|
-
var import_path = __toESM(require("path"), 1);
|
|
570
|
-
|
|
571
557
|
// node_modules/is-plain-obj/index.js
|
|
572
558
|
function isPlainObject(value) {
|
|
573
559
|
if (typeof value !== "object" || value === null) {
|
|
@@ -4949,13 +4935,13 @@ var logOutputSync = ({ serializedResult, fdNumber, state, verboseInfo, encoding,
|
|
|
4949
4935
|
}
|
|
4950
4936
|
};
|
|
4951
4937
|
var writeToFiles = (serializedResult, stdioItems, outputFiles) => {
|
|
4952
|
-
for (const { path:
|
|
4953
|
-
const pathString = typeof
|
|
4938
|
+
for (const { path: path12, append } of stdioItems.filter(({ type }) => FILE_TYPES.has(type))) {
|
|
4939
|
+
const pathString = typeof path12 === "string" ? path12 : path12.toString();
|
|
4954
4940
|
if (append || outputFiles.has(pathString)) {
|
|
4955
|
-
(0, import_node_fs4.appendFileSync)(
|
|
4941
|
+
(0, import_node_fs4.appendFileSync)(path12, serializedResult);
|
|
4956
4942
|
} else {
|
|
4957
4943
|
outputFiles.add(pathString);
|
|
4958
|
-
(0, import_node_fs4.writeFileSync)(
|
|
4944
|
+
(0, import_node_fs4.writeFileSync)(path12, serializedResult);
|
|
4959
4945
|
}
|
|
4960
4946
|
}
|
|
4961
4947
|
};
|
|
@@ -7343,131 +7329,11 @@ var {
|
|
|
7343
7329
|
getCancelSignal: getCancelSignal2
|
|
7344
7330
|
} = getIpcExport();
|
|
7345
7331
|
|
|
7346
|
-
// node_modules/@remixhq/core/dist/chunk-
|
|
7332
|
+
// node_modules/@remixhq/core/dist/chunk-WT6VRLXU.js
|
|
7347
7333
|
async function runGit(args, cwd) {
|
|
7348
7334
|
const res = await execa("git", args, { cwd, stderr: "ignore" });
|
|
7349
7335
|
return String(res.stdout || "").trim();
|
|
7350
7336
|
}
|
|
7351
|
-
async function runGitWithEnv(args, cwd, env) {
|
|
7352
|
-
const res = await execa("git", args, { cwd, env, stderr: "ignore" });
|
|
7353
|
-
return String(res.stdout || "").trim();
|
|
7354
|
-
}
|
|
7355
|
-
async function runGitRawWithEnv(args, cwd, env) {
|
|
7356
|
-
const res = await execa("git", args, { cwd, env, stderr: "ignore", stripFinalNewline: false });
|
|
7357
|
-
return String(res.stdout || "");
|
|
7358
|
-
}
|
|
7359
|
-
async function runGitDetailed(args, cwd) {
|
|
7360
|
-
const res = await execa("git", args, { cwd, reject: false });
|
|
7361
|
-
return {
|
|
7362
|
-
exitCode: res.exitCode ?? 1,
|
|
7363
|
-
stdout: String(res.stdout || ""),
|
|
7364
|
-
stderr: String(res.stderr || "")
|
|
7365
|
-
};
|
|
7366
|
-
}
|
|
7367
|
-
function normalizeRepoRelativePath(value) {
|
|
7368
|
-
const normalized = import_path.default.posix.normalize(value.replace(/\\/g, "/").trim());
|
|
7369
|
-
if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../") || import_path.default.posix.isAbsolute(normalized)) {
|
|
7370
|
-
throw new RemixError("Git returned an invalid repository-relative path.", {
|
|
7371
|
-
exitCode: 1,
|
|
7372
|
-
hint: `Path: ${value}`
|
|
7373
|
-
});
|
|
7374
|
-
}
|
|
7375
|
-
return normalized;
|
|
7376
|
-
}
|
|
7377
|
-
function resolveRepoRelativePath(repoRoot, relativePath) {
|
|
7378
|
-
return import_path.default.resolve(repoRoot, ...relativePath.split("/"));
|
|
7379
|
-
}
|
|
7380
|
-
function isWithinRepoRoot(repoRoot, targetPath) {
|
|
7381
|
-
const relative = import_path.default.relative(repoRoot, targetPath);
|
|
7382
|
-
return relative === "" || !relative.startsWith("..") && !import_path.default.isAbsolute(relative);
|
|
7383
|
-
}
|
|
7384
|
-
function sha256Hex(value) {
|
|
7385
|
-
return (0, import_crypto.createHash)("sha256").update(value).digest("hex");
|
|
7386
|
-
}
|
|
7387
|
-
function resolveGitReportedPath(cwd, reportedPath) {
|
|
7388
|
-
const value = reportedPath.trim();
|
|
7389
|
-
if (!value) {
|
|
7390
|
-
throw new RemixError("Git returned an empty internal path.", { exitCode: 1 });
|
|
7391
|
-
}
|
|
7392
|
-
return import_path.default.isAbsolute(value) ? value : import_path.default.resolve(cwd, value);
|
|
7393
|
-
}
|
|
7394
|
-
function parseGitNameStatusZ(stdout) {
|
|
7395
|
-
const tokens = stdout.split("\0");
|
|
7396
|
-
const entries = [];
|
|
7397
|
-
for (let i2 = 0; i2 < tokens.length; i2 += 1) {
|
|
7398
|
-
const rawToken = tokens[i2];
|
|
7399
|
-
if (!rawToken) continue;
|
|
7400
|
-
let status = rawToken;
|
|
7401
|
-
let inlinePath = null;
|
|
7402
|
-
const tabIdx = rawToken.indexOf(" ");
|
|
7403
|
-
if (tabIdx >= 0) {
|
|
7404
|
-
status = rawToken.slice(0, tabIdx);
|
|
7405
|
-
inlinePath = rawToken.slice(tabIdx + 1) || null;
|
|
7406
|
-
}
|
|
7407
|
-
const kind = status[0]?.toUpperCase() ?? "";
|
|
7408
|
-
if (kind === "R" || kind === "C") {
|
|
7409
|
-
const oldPath = inlinePath ?? tokens[++i2] ?? "";
|
|
7410
|
-
const newPath = tokens[++i2] ?? "";
|
|
7411
|
-
if (!newPath) continue;
|
|
7412
|
-
entries.push({
|
|
7413
|
-
status,
|
|
7414
|
-
path: normalizeRepoRelativePath(newPath),
|
|
7415
|
-
oldPath: oldPath ? normalizeRepoRelativePath(oldPath) : null
|
|
7416
|
-
});
|
|
7417
|
-
continue;
|
|
7418
|
-
}
|
|
7419
|
-
const nextPath = inlinePath ?? tokens[++i2] ?? "";
|
|
7420
|
-
if (!nextPath) continue;
|
|
7421
|
-
entries.push({
|
|
7422
|
-
status,
|
|
7423
|
-
path: normalizeRepoRelativePath(nextPath),
|
|
7424
|
-
oldPath: null
|
|
7425
|
-
});
|
|
7426
|
-
}
|
|
7427
|
-
return entries;
|
|
7428
|
-
}
|
|
7429
|
-
function buildStagePlan(entries) {
|
|
7430
|
-
const updatePaths = /* @__PURE__ */ new Set();
|
|
7431
|
-
const addPaths = /* @__PURE__ */ new Set();
|
|
7432
|
-
for (const entry of entries) {
|
|
7433
|
-
const kind = entry.status[0]?.toUpperCase() ?? "";
|
|
7434
|
-
if (kind === "A" || kind === "C") {
|
|
7435
|
-
addPaths.add(entry.path);
|
|
7436
|
-
continue;
|
|
7437
|
-
}
|
|
7438
|
-
if (kind === "R") {
|
|
7439
|
-
if (entry.oldPath) updatePaths.add(entry.oldPath);
|
|
7440
|
-
addPaths.add(entry.path);
|
|
7441
|
-
continue;
|
|
7442
|
-
}
|
|
7443
|
-
updatePaths.add(entry.path);
|
|
7444
|
-
}
|
|
7445
|
-
const expectedPaths = /* @__PURE__ */ new Set([...updatePaths, ...addPaths]);
|
|
7446
|
-
return {
|
|
7447
|
-
updatePaths: Array.from(updatePaths).sort(),
|
|
7448
|
-
addPaths: Array.from(addPaths).sort(),
|
|
7449
|
-
expectedPaths: Array.from(expectedPaths).sort()
|
|
7450
|
-
};
|
|
7451
|
-
}
|
|
7452
|
-
function classifyGitApplyFailure(detail) {
|
|
7453
|
-
const normalized = String(detail ?? "").toLowerCase();
|
|
7454
|
-
if (normalized.includes("corrupt patch") || normalized.includes("patch with only garbage") || normalized.includes("unrecognized input") || normalized.includes("malformed patch") || normalized.includes("patch fragment without header")) {
|
|
7455
|
-
return "malformed_patch";
|
|
7456
|
-
}
|
|
7457
|
-
if (normalized.includes("patch failed") || normalized.includes("does not apply") || normalized.includes("merge conflict") || normalized.includes("conflict")) {
|
|
7458
|
-
return "apply_conflict";
|
|
7459
|
-
}
|
|
7460
|
-
return "unknown";
|
|
7461
|
-
}
|
|
7462
|
-
async function pruneEmptyParentDirectories(repoRoot, filePath) {
|
|
7463
|
-
let current = import_path.default.dirname(filePath);
|
|
7464
|
-
while (current !== repoRoot && isWithinRepoRoot(repoRoot, current)) {
|
|
7465
|
-
const entries = await import_promises12.default.readdir(current).catch(() => null);
|
|
7466
|
-
if (!entries || entries.length > 0) return;
|
|
7467
|
-
await import_promises12.default.rmdir(current).catch(() => void 0);
|
|
7468
|
-
current = import_path.default.dirname(current);
|
|
7469
|
-
}
|
|
7470
|
-
}
|
|
7471
7337
|
async function findGitRoot(startDir) {
|
|
7472
7338
|
try {
|
|
7473
7339
|
const root = await runGit(["rev-parse", "--show-toplevel"], startDir);
|
|
@@ -7480,17 +7346,6 @@ async function findGitRoot(startDir) {
|
|
|
7480
7346
|
});
|
|
7481
7347
|
}
|
|
7482
7348
|
}
|
|
7483
|
-
async function getGitCommonDir(cwd) {
|
|
7484
|
-
try {
|
|
7485
|
-
const commonDir = await runGit(["rev-parse", "--git-common-dir"], cwd);
|
|
7486
|
-
return resolveGitReportedPath(cwd, commonDir);
|
|
7487
|
-
} catch {
|
|
7488
|
-
throw new RemixError("Failed to resolve the git common directory.", {
|
|
7489
|
-
exitCode: 1,
|
|
7490
|
-
hint: "Ensure the current working directory is inside a valid git repository."
|
|
7491
|
-
});
|
|
7492
|
-
}
|
|
7493
|
-
}
|
|
7494
7349
|
async function getCurrentBranch(cwd) {
|
|
7495
7350
|
try {
|
|
7496
7351
|
const branch = await runGit(["branch", "--show-current"], cwd);
|
|
@@ -7499,121 +7354,6 @@ async function getCurrentBranch(cwd) {
|
|
|
7499
7354
|
return null;
|
|
7500
7355
|
}
|
|
7501
7356
|
}
|
|
7502
|
-
async function listUntrackedFiles(cwd) {
|
|
7503
|
-
try {
|
|
7504
|
-
const out = await runGit(["ls-files", "--others", "--exclude-standard"], cwd);
|
|
7505
|
-
return out.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
7506
|
-
} catch {
|
|
7507
|
-
return [];
|
|
7508
|
-
}
|
|
7509
|
-
}
|
|
7510
|
-
async function getWorkspaceDiff(cwd) {
|
|
7511
|
-
const snapshot = await getWorkspaceSnapshot(cwd);
|
|
7512
|
-
return {
|
|
7513
|
-
diff: snapshot.diff,
|
|
7514
|
-
includedUntrackedPaths: snapshot.includedUntrackedPaths
|
|
7515
|
-
};
|
|
7516
|
-
}
|
|
7517
|
-
async function getWorkspaceSnapshot(cwd) {
|
|
7518
|
-
const headCommitHash = await getHeadCommitHash(cwd);
|
|
7519
|
-
if (!headCommitHash) {
|
|
7520
|
-
throw new RemixError("Failed to resolve local HEAD commit.", { exitCode: 1 });
|
|
7521
|
-
}
|
|
7522
|
-
const includedUntrackedPaths = Array.from(new Set((await listUntrackedFiles(cwd)).map((entry) => normalizeRepoRelativePath(entry))));
|
|
7523
|
-
const tempDir = await import_promises12.default.mkdtemp(import_path.default.join(import_os.default.tmpdir(), "remix-index-"));
|
|
7524
|
-
const tempIndexPath = import_path.default.join(tempDir, "index");
|
|
7525
|
-
const env = { ...process.env, GIT_INDEX_FILE: tempIndexPath };
|
|
7526
|
-
try {
|
|
7527
|
-
try {
|
|
7528
|
-
await runGitWithEnv(["read-tree", "HEAD"], cwd, env);
|
|
7529
|
-
await runGitWithEnv(["add", "-A", "--", "."], cwd, env);
|
|
7530
|
-
const diff = await runGitRawWithEnv(["diff", "--binary", "--no-ext-diff", "--cached", "HEAD"], cwd, env);
|
|
7531
|
-
const nameStatus = await runGitRawWithEnv(["diff", "--name-status", "--find-renames", "-z", "--cached", "HEAD"], cwd, env);
|
|
7532
|
-
const pathEntries = parseGitNameStatusZ(nameStatus);
|
|
7533
|
-
return {
|
|
7534
|
-
diff,
|
|
7535
|
-
diffSha256: sha256Hex(diff),
|
|
7536
|
-
includedUntrackedPaths,
|
|
7537
|
-
pathEntries,
|
|
7538
|
-
stagePlan: buildStagePlan(pathEntries)
|
|
7539
|
-
};
|
|
7540
|
-
} catch {
|
|
7541
|
-
throw new RemixError("Failed to generate workspace diff.", {
|
|
7542
|
-
exitCode: 1,
|
|
7543
|
-
hint: "Git could not snapshot the current workspace into an isolated index."
|
|
7544
|
-
});
|
|
7545
|
-
}
|
|
7546
|
-
} finally {
|
|
7547
|
-
await import_promises12.default.rm(tempDir, { recursive: true, force: true }).catch(() => void 0);
|
|
7548
|
-
}
|
|
7549
|
-
}
|
|
7550
|
-
async function writeTempUnifiedDiffBackup(diff, prefix = "remix-add-backup") {
|
|
7551
|
-
const safePrefix = prefix.replace(/[^a-zA-Z0-9._-]+/g, "-") || "remix-add-backup";
|
|
7552
|
-
const tmpDir = await import_promises12.default.mkdtemp(import_path.default.join(import_os.default.tmpdir(), `${safePrefix}-`));
|
|
7553
|
-
const backupPath = import_path.default.join(tmpDir, "submitted.diff");
|
|
7554
|
-
await import_promises12.default.writeFile(backupPath, diff, "utf8");
|
|
7555
|
-
return { backupPath, diffSha256: sha256Hex(diff) };
|
|
7556
|
-
}
|
|
7557
|
-
async function validateUnifiedDiff(cwd, diff) {
|
|
7558
|
-
const { backupPath } = await writeTempUnifiedDiffBackup(diff, "remix-validate-diff");
|
|
7559
|
-
try {
|
|
7560
|
-
const applyRes = await runGitDetailed(["apply", "--check", "--index", "--3way", backupPath], cwd);
|
|
7561
|
-
if (applyRes.exitCode === 0) {
|
|
7562
|
-
return { ok: true };
|
|
7563
|
-
}
|
|
7564
|
-
const detail = [applyRes.stderr.trim(), applyRes.stdout.trim()].filter(Boolean).join("\n\n") || null;
|
|
7565
|
-
return {
|
|
7566
|
-
ok: false,
|
|
7567
|
-
kind: classifyGitApplyFailure(detail),
|
|
7568
|
-
detail
|
|
7569
|
-
};
|
|
7570
|
-
} finally {
|
|
7571
|
-
await import_promises12.default.rm(import_path.default.dirname(backupPath), { recursive: true, force: true }).catch(() => void 0);
|
|
7572
|
-
}
|
|
7573
|
-
}
|
|
7574
|
-
async function preserveWorkspaceChanges(cwd, prefix = "remix-add-preserve") {
|
|
7575
|
-
const baseHeadCommitHash = await getHeadCommitHash(cwd);
|
|
7576
|
-
if (!baseHeadCommitHash) {
|
|
7577
|
-
throw new RemixError("Failed to resolve local HEAD before preserving workspace changes.", { exitCode: 1 });
|
|
7578
|
-
}
|
|
7579
|
-
const snapshot = await getWorkspaceSnapshot(cwd);
|
|
7580
|
-
const { backupPath, diffSha256 } = await writeTempUnifiedDiffBackup(snapshot.diff, prefix);
|
|
7581
|
-
return {
|
|
7582
|
-
baseHeadCommitHash,
|
|
7583
|
-
preservedDiffPath: backupPath,
|
|
7584
|
-
preservedDiffSha256: diffSha256,
|
|
7585
|
-
includedUntrackedPaths: snapshot.includedUntrackedPaths,
|
|
7586
|
-
stagePlan: snapshot.stagePlan
|
|
7587
|
-
};
|
|
7588
|
-
}
|
|
7589
|
-
async function reapplyPreservedWorkspaceChanges(cwd, preserved, operation = "`remix collab add`") {
|
|
7590
|
-
const patchFilePath = preserved.preservedDiffPath;
|
|
7591
|
-
const tempPatchHash = await import_promises12.default.readFile(patchFilePath, "utf8").then((content) => sha256Hex(content)).catch(() => null);
|
|
7592
|
-
if (!tempPatchHash) {
|
|
7593
|
-
return { status: "failed", detail: "Preserved diff artifact is missing." };
|
|
7594
|
-
}
|
|
7595
|
-
if (tempPatchHash !== preserved.preservedDiffSha256) {
|
|
7596
|
-
return { status: "failed", detail: "Preserved diff artifact failed integrity verification." };
|
|
7597
|
-
}
|
|
7598
|
-
const applyRes = await runGitDetailed(["apply", "--index", "--3way", patchFilePath], cwd);
|
|
7599
|
-
if (applyRes.exitCode !== 0) {
|
|
7600
|
-
await runGitDetailed(["reset", "--hard", "HEAD"], cwd).catch(() => void 0);
|
|
7601
|
-
await runGitDetailed(["clean", "-fd"], cwd).catch(() => void 0);
|
|
7602
|
-
const detail = [applyRes.stderr.trim(), applyRes.stdout.trim()].filter(Boolean).join("\n\n") || null;
|
|
7603
|
-
return { status: "conflict", detail };
|
|
7604
|
-
}
|
|
7605
|
-
const unstageRes = await runGitDetailed(["reset", "--mixed", "HEAD", "--", "."], cwd);
|
|
7606
|
-
if (unstageRes.exitCode !== 0) {
|
|
7607
|
-
await runGitDetailed(["reset", "--hard", "HEAD"], cwd).catch(() => void 0);
|
|
7608
|
-
await runGitDetailed(["clean", "-fd"], cwd).catch(() => void 0);
|
|
7609
|
-
const detail = [unstageRes.stderr.trim(), unstageRes.stdout.trim()].filter(Boolean).join("\n\n") || null;
|
|
7610
|
-
return {
|
|
7611
|
-
status: "failed",
|
|
7612
|
-
detail: detail || `Git could not restore the working tree state while running ${operation}.`
|
|
7613
|
-
};
|
|
7614
|
-
}
|
|
7615
|
-
return { status: "clean" };
|
|
7616
|
-
}
|
|
7617
7357
|
async function getHeadCommitHash(cwd) {
|
|
7618
7358
|
try {
|
|
7619
7359
|
const hash = await runGit(["rev-parse", "HEAD"], cwd);
|
|
@@ -7631,157 +7371,6 @@ async function getWorktreeStatus(cwd) {
|
|
|
7631
7371
|
return { isClean: false, entries: [] };
|
|
7632
7372
|
}
|
|
7633
7373
|
}
|
|
7634
|
-
async function captureRepoSnapshot(cwd, options) {
|
|
7635
|
-
const [branch, headCommitHash, worktreeStatus] = await Promise.all([
|
|
7636
|
-
getCurrentBranch(cwd),
|
|
7637
|
-
getHeadCommitHash(cwd),
|
|
7638
|
-
getWorktreeStatus(cwd)
|
|
7639
|
-
]);
|
|
7640
|
-
const workspaceDiffSha256 = options?.includeWorkspaceDiffHash ? (await getWorkspaceSnapshot(cwd)).diffSha256 : null;
|
|
7641
|
-
return {
|
|
7642
|
-
branch,
|
|
7643
|
-
headCommitHash,
|
|
7644
|
-
statusEntries: worktreeStatus.entries,
|
|
7645
|
-
statusSha256: sha256Hex(worktreeStatus.entries.join("\n")),
|
|
7646
|
-
workspaceDiffSha256
|
|
7647
|
-
};
|
|
7648
|
-
}
|
|
7649
|
-
async function assertRepoSnapshotUnchanged(cwd, snapshot, params) {
|
|
7650
|
-
const current = await captureRepoSnapshot(cwd, {
|
|
7651
|
-
includeWorkspaceDiffHash: snapshot.workspaceDiffSha256 !== null
|
|
7652
|
-
});
|
|
7653
|
-
const changes = [];
|
|
7654
|
-
if (current.branch !== snapshot.branch) {
|
|
7655
|
-
changes.push(`Branch changed: ${snapshot.branch ?? "(detached)"} -> ${current.branch ?? "(detached)"}`);
|
|
7656
|
-
}
|
|
7657
|
-
if (current.headCommitHash !== snapshot.headCommitHash) {
|
|
7658
|
-
changes.push(`HEAD changed: ${snapshot.headCommitHash ?? "(missing)"} -> ${current.headCommitHash ?? "(missing)"}`);
|
|
7659
|
-
}
|
|
7660
|
-
if (current.statusSha256 !== snapshot.statusSha256) {
|
|
7661
|
-
changes.push("Working tree status changed.");
|
|
7662
|
-
}
|
|
7663
|
-
if (snapshot.workspaceDiffSha256 !== null && current.workspaceDiffSha256 !== snapshot.workspaceDiffSha256) {
|
|
7664
|
-
changes.push("Workspace diff changed.");
|
|
7665
|
-
}
|
|
7666
|
-
if (changes.length === 0) return;
|
|
7667
|
-
const operation = params?.operation?.trim() || "this Remix operation";
|
|
7668
|
-
const hint = [
|
|
7669
|
-
`Operation: ${operation}`,
|
|
7670
|
-
...changes,
|
|
7671
|
-
params?.recoveryHint?.trim() || "Review the local changes, then rerun the operation."
|
|
7672
|
-
].filter(Boolean).join("\n");
|
|
7673
|
-
throw new RemixError("Repository state changed during the operation.", {
|
|
7674
|
-
code: REMIX_ERROR_CODES.REPO_STATE_CHANGED_DURING_OPERATION,
|
|
7675
|
-
exitCode: 2,
|
|
7676
|
-
hint
|
|
7677
|
-
});
|
|
7678
|
-
}
|
|
7679
|
-
async function ensureCleanWorktree(cwd, operation = "`remix collab sync`") {
|
|
7680
|
-
const status = await getWorktreeStatus(cwd);
|
|
7681
|
-
if (status.isClean) return;
|
|
7682
|
-
const preview = status.entries.slice(0, 10).join("\n");
|
|
7683
|
-
const suffix = status.entries.length > 10 ? `
|
|
7684
|
-
...and ${status.entries.length - 10} more` : "";
|
|
7685
|
-
throw new RemixError(`Working tree must be clean before running ${operation}.`, {
|
|
7686
|
-
exitCode: 2,
|
|
7687
|
-
hint: `Commit, stash, or discard local changes first.
|
|
7688
|
-
|
|
7689
|
-
${preview}${suffix}`
|
|
7690
|
-
});
|
|
7691
|
-
}
|
|
7692
|
-
async function discardTrackedChanges(cwd, operation = "`remix collab add`") {
|
|
7693
|
-
const res = await runGitDetailed(["reset", "--hard", "HEAD"], cwd);
|
|
7694
|
-
if (res.exitCode !== 0) {
|
|
7695
|
-
const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n\n");
|
|
7696
|
-
throw new RemixError(`Failed to discard local tracked changes while running ${operation}.`, {
|
|
7697
|
-
exitCode: 1,
|
|
7698
|
-
hint: detail || "Git could not reset tracked changes back to HEAD."
|
|
7699
|
-
});
|
|
7700
|
-
}
|
|
7701
|
-
const hash = await getHeadCommitHash(cwd);
|
|
7702
|
-
if (!hash) {
|
|
7703
|
-
throw new RemixError("Failed to resolve local HEAD after discarding tracked changes.", { exitCode: 1 });
|
|
7704
|
-
}
|
|
7705
|
-
return hash;
|
|
7706
|
-
}
|
|
7707
|
-
async function discardCapturedUntrackedChanges(repoRoot, capturedPaths) {
|
|
7708
|
-
const normalizedCapturedPaths = Array.from(new Set(capturedPaths.map((entry) => normalizeRepoRelativePath(entry))));
|
|
7709
|
-
if (normalizedCapturedPaths.length === 0) {
|
|
7710
|
-
return { removedPaths: [] };
|
|
7711
|
-
}
|
|
7712
|
-
const currentUntracked = new Set((await listUntrackedFiles(repoRoot)).map((entry) => normalizeRepoRelativePath(entry)));
|
|
7713
|
-
const removedPaths = [];
|
|
7714
|
-
for (const relativePath of normalizedCapturedPaths) {
|
|
7715
|
-
if (!currentUntracked.has(relativePath)) continue;
|
|
7716
|
-
const absolutePath = resolveRepoRelativePath(repoRoot, relativePath);
|
|
7717
|
-
if (!isWithinRepoRoot(repoRoot, absolutePath)) {
|
|
7718
|
-
throw new RemixError("Refusing to delete a path outside the repository root.", {
|
|
7719
|
-
exitCode: 1,
|
|
7720
|
-
hint: `Path: ${relativePath}`
|
|
7721
|
-
});
|
|
7722
|
-
}
|
|
7723
|
-
await import_promises12.default.rm(absolutePath, { recursive: true, force: true }).catch((err) => {
|
|
7724
|
-
throw new RemixError("Failed to remove a captured untracked path before syncing.", {
|
|
7725
|
-
exitCode: 1,
|
|
7726
|
-
hint: err instanceof Error ? `${relativePath}
|
|
7727
|
-
|
|
7728
|
-
${err.message}` : relativePath
|
|
7729
|
-
});
|
|
7730
|
-
});
|
|
7731
|
-
removedPaths.push(relativePath);
|
|
7732
|
-
await pruneEmptyParentDirectories(repoRoot, absolutePath);
|
|
7733
|
-
}
|
|
7734
|
-
return { removedPaths };
|
|
7735
|
-
}
|
|
7736
|
-
async function requireCurrentBranch(cwd) {
|
|
7737
|
-
const branch = await getCurrentBranch(cwd);
|
|
7738
|
-
if (!branch) {
|
|
7739
|
-
throw new RemixError("`remix collab sync` requires a checked out local branch.", {
|
|
7740
|
-
exitCode: 2,
|
|
7741
|
-
hint: "Checkout a branch before syncing."
|
|
7742
|
-
});
|
|
7743
|
-
}
|
|
7744
|
-
return branch;
|
|
7745
|
-
}
|
|
7746
|
-
async function importGitBundle(cwd, bundlePath, bundleRef) {
|
|
7747
|
-
const verifyRes = await runGitDetailed(["bundle", "verify", bundlePath], cwd);
|
|
7748
|
-
if (verifyRes.exitCode !== 0) {
|
|
7749
|
-
const detail = [verifyRes.stderr.trim(), verifyRes.stdout.trim()].filter(Boolean).join("\n\n");
|
|
7750
|
-
throw new RemixError("Failed to verify sync bundle.", {
|
|
7751
|
-
exitCode: 1,
|
|
7752
|
-
hint: detail || "Git bundle verification failed."
|
|
7753
|
-
});
|
|
7754
|
-
}
|
|
7755
|
-
const fetchRes = await runGitDetailed(["fetch", "--quiet", bundlePath, bundleRef], cwd);
|
|
7756
|
-
if (fetchRes.exitCode !== 0) {
|
|
7757
|
-
const detail = [fetchRes.stderr.trim(), fetchRes.stdout.trim()].filter(Boolean).join("\n\n");
|
|
7758
|
-
throw new RemixError("Failed to import sync bundle.", {
|
|
7759
|
-
exitCode: 1,
|
|
7760
|
-
hint: detail || "Git could not fetch objects from the sync bundle."
|
|
7761
|
-
});
|
|
7762
|
-
}
|
|
7763
|
-
}
|
|
7764
|
-
async function ensureCommitExists(cwd, commitHash) {
|
|
7765
|
-
const res = await runGitDetailed(["cat-file", "-e", `${commitHash}^{commit}`], cwd);
|
|
7766
|
-
if (res.exitCode === 0) return;
|
|
7767
|
-
throw new RemixError("Expected target commit is missing after bundle import.", {
|
|
7768
|
-
exitCode: 1,
|
|
7769
|
-
hint: `Commit ${commitHash} is not available in the local repository.`
|
|
7770
|
-
});
|
|
7771
|
-
}
|
|
7772
|
-
async function fastForwardToCommit(cwd, commitHash) {
|
|
7773
|
-
const res = await runGitDetailed(["merge", "--ff-only", commitHash], cwd);
|
|
7774
|
-
if (res.exitCode !== 0) {
|
|
7775
|
-
const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n\n");
|
|
7776
|
-
throw new RemixError("Failed to fast-forward local branch.", {
|
|
7777
|
-
exitCode: 1,
|
|
7778
|
-
hint: detail || "Git could not fast-forward to the target commit."
|
|
7779
|
-
});
|
|
7780
|
-
}
|
|
7781
|
-
const hash = await getHeadCommitHash(cwd);
|
|
7782
|
-
if (!hash) throw new RemixError("Failed to resolve local HEAD after fast-forward sync.", { exitCode: 1 });
|
|
7783
|
-
return hash;
|
|
7784
|
-
}
|
|
7785
7374
|
function summarizeUnifiedDiff(diff) {
|
|
7786
7375
|
const lines = diff.split("\n");
|
|
7787
7376
|
let changedFilesCount = 0;
|
|
@@ -7795,21 +7384,21 @@ function summarizeUnifiedDiff(diff) {
|
|
|
7795
7384
|
return { changedFilesCount, insertions, deletions };
|
|
7796
7385
|
}
|
|
7797
7386
|
|
|
7798
|
-
// node_modules/@remixhq/core/dist/chunk-
|
|
7387
|
+
// node_modules/@remixhq/core/dist/chunk-YCFLOHJV.js
|
|
7388
|
+
var import_promises12 = __toESM(require("fs/promises"), 1);
|
|
7389
|
+
var import_path = __toESM(require("path"), 1);
|
|
7799
7390
|
var import_promises13 = __toESM(require("fs/promises"), 1);
|
|
7800
7391
|
var import_path2 = __toESM(require("path"), 1);
|
|
7801
|
-
var import_promises14 = __toESM(require("fs/promises"), 1);
|
|
7802
|
-
var import_path3 = __toESM(require("path"), 1);
|
|
7803
7392
|
async function writeJsonAtomic(filePath, value) {
|
|
7804
|
-
const dir =
|
|
7805
|
-
await
|
|
7393
|
+
const dir = import_path2.default.dirname(filePath);
|
|
7394
|
+
await import_promises13.default.mkdir(dir, { recursive: true });
|
|
7806
7395
|
const tmp = `${filePath}.tmp-${Date.now()}`;
|
|
7807
|
-
await
|
|
7396
|
+
await import_promises13.default.writeFile(tmp, `${JSON.stringify(value, null, 2)}
|
|
7808
7397
|
`, "utf8");
|
|
7809
|
-
await
|
|
7398
|
+
await import_promises13.default.rename(tmp, filePath);
|
|
7810
7399
|
}
|
|
7811
7400
|
function getCollabBindingPath(repoRoot) {
|
|
7812
|
-
return
|
|
7401
|
+
return import_path.default.join(repoRoot, ".remix", "config.json");
|
|
7813
7402
|
}
|
|
7814
7403
|
function buildBindingFileV3(params) {
|
|
7815
7404
|
return {
|
|
@@ -7817,7 +7406,8 @@ function buildBindingFileV3(params) {
|
|
|
7817
7406
|
repoFingerprint: params.repoFingerprint,
|
|
7818
7407
|
remoteUrl: params.remoteUrl,
|
|
7819
7408
|
defaultBranch: params.defaultBranch,
|
|
7820
|
-
branchBindings: params.branchBindings
|
|
7409
|
+
branchBindings: params.branchBindings,
|
|
7410
|
+
...params.explicitRootBinding ? { explicitRootBinding: params.explicitRootBinding } : {}
|
|
7821
7411
|
};
|
|
7822
7412
|
}
|
|
7823
7413
|
function normalizeBranchName(value) {
|
|
@@ -7836,7 +7426,7 @@ function normalizeBranchBinding(value) {
|
|
|
7836
7426
|
upstreamAppId: value.upstreamAppId,
|
|
7837
7427
|
threadId: value.threadId ?? null,
|
|
7838
7428
|
laneId: value.laneId ?? null,
|
|
7839
|
-
bindingMode: value.bindingMode === "legacy" ? "legacy" : "lane"
|
|
7429
|
+
bindingMode: value.bindingMode === "legacy" ? "legacy" : value.bindingMode === "explicit_root" ? "explicit_root" : "lane"
|
|
7840
7430
|
};
|
|
7841
7431
|
}
|
|
7842
7432
|
function buildResolvedBinding(params) {
|
|
@@ -7871,7 +7461,7 @@ async function readCollabBindingState(repoRoot, options) {
|
|
|
7871
7461
|
try {
|
|
7872
7462
|
const persist = options?.persist === true;
|
|
7873
7463
|
const filePath = getCollabBindingPath(repoRoot);
|
|
7874
|
-
const raw = await
|
|
7464
|
+
const raw = await import_promises12.default.readFile(filePath, "utf8");
|
|
7875
7465
|
const parsed = JSON.parse(raw);
|
|
7876
7466
|
if (!parsed || typeof parsed !== "object") return null;
|
|
7877
7467
|
const currentBranch = normalizeBranchName(await getCurrentBranch(repoRoot).catch(() => null));
|
|
@@ -7910,6 +7500,7 @@ async function readCollabBindingState(repoRoot, options) {
|
|
|
7910
7500
|
defaultBranch: migratedFile.defaultBranch,
|
|
7911
7501
|
currentBranch,
|
|
7912
7502
|
branchBindings: migratedFile.branchBindings,
|
|
7503
|
+
explicitRootBinding: null,
|
|
7913
7504
|
binding: buildResolvedBinding({
|
|
7914
7505
|
fallbackProjectId: projectId,
|
|
7915
7506
|
repoFingerprint: migratedFile.repoFingerprint,
|
|
@@ -7953,7 +7544,22 @@ async function readCollabBindingState(repoRoot, options) {
|
|
|
7953
7544
|
};
|
|
7954
7545
|
shouldPersistNormalizedBranchBindings = true;
|
|
7955
7546
|
}
|
|
7956
|
-
|
|
7547
|
+
let explicitRootBinding = normalizeBranchBinding(file.explicitRootBinding ?? null);
|
|
7548
|
+
if (explicitRootBinding && !explicitRootBinding.projectId && legacyProjectId) {
|
|
7549
|
+
explicitRootBinding = {
|
|
7550
|
+
...explicitRootBinding,
|
|
7551
|
+
projectId: legacyProjectId
|
|
7552
|
+
};
|
|
7553
|
+
shouldPersistNormalizedBranchBindings = true;
|
|
7554
|
+
}
|
|
7555
|
+
if (explicitRootBinding && explicitRootBinding.bindingMode !== "explicit_root") {
|
|
7556
|
+
explicitRootBinding = {
|
|
7557
|
+
...explicitRootBinding,
|
|
7558
|
+
bindingMode: "explicit_root"
|
|
7559
|
+
};
|
|
7560
|
+
shouldPersistNormalizedBranchBindings = true;
|
|
7561
|
+
}
|
|
7562
|
+
if (persist && ("explicitBinding" in file || "explicitRootBinding" in file || shouldPersistNormalizedBranchBindings || parsed.schemaVersion === 2)) {
|
|
7957
7563
|
try {
|
|
7958
7564
|
await writeJsonAtomic(
|
|
7959
7565
|
filePath,
|
|
@@ -7961,7 +7567,8 @@ async function readCollabBindingState(repoRoot, options) {
|
|
|
7961
7567
|
repoFingerprint: file.repoFingerprint ?? null,
|
|
7962
7568
|
remoteUrl: file.remoteUrl ?? null,
|
|
7963
7569
|
defaultBranch: file.defaultBranch ?? null,
|
|
7964
|
-
branchBindings
|
|
7570
|
+
branchBindings,
|
|
7571
|
+
explicitRootBinding
|
|
7965
7572
|
})
|
|
7966
7573
|
);
|
|
7967
7574
|
} catch {
|
|
@@ -7972,8 +7579,23 @@ async function readCollabBindingState(repoRoot, options) {
|
|
|
7972
7579
|
branchBindings,
|
|
7973
7580
|
currentBranch: resolvedBranch,
|
|
7974
7581
|
defaultBranch: normalizeBranchName(file.defaultBranch),
|
|
7975
|
-
legacyProjectId
|
|
7582
|
+
legacyProjectId: explicitRootBinding?.projectId ?? legacyProjectId
|
|
7976
7583
|
});
|
|
7584
|
+
const resolvedBinding = buildResolvedBinding({
|
|
7585
|
+
fallbackProjectId,
|
|
7586
|
+
repoFingerprint: file.repoFingerprint ?? null,
|
|
7587
|
+
remoteUrl: file.remoteUrl ?? null,
|
|
7588
|
+
defaultBranch: file.defaultBranch ?? null,
|
|
7589
|
+
branchName: resolvedBranch,
|
|
7590
|
+
binding: resolvedBranch ? branchBindings[resolvedBranch] ?? null : null
|
|
7591
|
+
}) ?? (resolvedBranch && resolvedBranch === normalizeBranchName(file.defaultBranch) && explicitRootBinding ? buildResolvedBinding({
|
|
7592
|
+
fallbackProjectId,
|
|
7593
|
+
repoFingerprint: file.repoFingerprint ?? null,
|
|
7594
|
+
remoteUrl: file.remoteUrl ?? null,
|
|
7595
|
+
defaultBranch: file.defaultBranch ?? null,
|
|
7596
|
+
branchName: normalizeBranchName(file.defaultBranch),
|
|
7597
|
+
binding: explicitRootBinding
|
|
7598
|
+
}) : null);
|
|
7977
7599
|
return {
|
|
7978
7600
|
schemaVersion: parsed.schemaVersion,
|
|
7979
7601
|
projectId: fallbackProjectId,
|
|
@@ -7982,14 +7604,15 @@ async function readCollabBindingState(repoRoot, options) {
|
|
|
7982
7604
|
defaultBranch: file.defaultBranch ?? null,
|
|
7983
7605
|
currentBranch,
|
|
7984
7606
|
branchBindings,
|
|
7985
|
-
|
|
7607
|
+
explicitRootBinding: buildResolvedBinding({
|
|
7986
7608
|
fallbackProjectId,
|
|
7987
7609
|
repoFingerprint: file.repoFingerprint ?? null,
|
|
7988
7610
|
remoteUrl: file.remoteUrl ?? null,
|
|
7989
7611
|
defaultBranch: file.defaultBranch ?? null,
|
|
7990
|
-
branchName:
|
|
7991
|
-
binding:
|
|
7992
|
-
})
|
|
7612
|
+
branchName: normalizeBranchName(file.defaultBranch),
|
|
7613
|
+
binding: explicitRootBinding
|
|
7614
|
+
}),
|
|
7615
|
+
binding: resolvedBinding
|
|
7993
7616
|
};
|
|
7994
7617
|
} catch {
|
|
7995
7618
|
return null;
|
|
@@ -8013,28 +7636,44 @@ async function writeCollabBinding(repoRoot, binding) {
|
|
|
8013
7636
|
laneId: binding.laneId ?? null,
|
|
8014
7637
|
bindingMode: binding.bindingMode ?? "lane"
|
|
8015
7638
|
};
|
|
7639
|
+
const explicitRootBinding = binding.bindingMode === "explicit_root" ? {
|
|
7640
|
+
...branchBindings[branchName],
|
|
7641
|
+
bindingMode: "explicit_root"
|
|
7642
|
+
} : existing?.explicitRootBinding ? {
|
|
7643
|
+
projectId: existing.explicitRootBinding.projectId,
|
|
7644
|
+
currentAppId: existing.explicitRootBinding.currentAppId,
|
|
7645
|
+
upstreamAppId: existing.explicitRootBinding.upstreamAppId,
|
|
7646
|
+
threadId: existing.explicitRootBinding.threadId,
|
|
7647
|
+
laneId: existing.explicitRootBinding.laneId,
|
|
7648
|
+
bindingMode: "explicit_root"
|
|
7649
|
+
} : null;
|
|
8016
7650
|
await writeJsonAtomic(
|
|
8017
7651
|
filePath,
|
|
8018
7652
|
buildBindingFileV3({
|
|
8019
7653
|
repoFingerprint: binding.repoFingerprint ?? null,
|
|
8020
7654
|
remoteUrl: binding.remoteUrl ?? null,
|
|
8021
7655
|
defaultBranch: binding.defaultBranch ?? null,
|
|
8022
|
-
branchBindings
|
|
7656
|
+
branchBindings,
|
|
7657
|
+
explicitRootBinding
|
|
8023
7658
|
})
|
|
8024
7659
|
);
|
|
8025
7660
|
return filePath;
|
|
8026
7661
|
}
|
|
8027
7662
|
|
|
8028
7663
|
// node_modules/@remixhq/core/dist/collab.js
|
|
8029
|
-
var
|
|
7664
|
+
var import_promises14 = __toESM(require("fs/promises"), 1);
|
|
7665
|
+
var import_path3 = __toESM(require("path"), 1);
|
|
7666
|
+
var import_crypto = require("crypto");
|
|
7667
|
+
var import_os = __toESM(require("os"), 1);
|
|
8030
7668
|
var import_path4 = __toESM(require("path"), 1);
|
|
8031
7669
|
var import_crypto2 = require("crypto");
|
|
8032
|
-
var
|
|
7670
|
+
var import_promises15 = __toESM(require("fs/promises"), 1);
|
|
8033
7671
|
var import_os2 = __toESM(require("os"), 1);
|
|
8034
7672
|
var import_path5 = __toESM(require("path"), 1);
|
|
8035
|
-
var
|
|
8036
|
-
var
|
|
7673
|
+
var import_crypto3 = require("crypto");
|
|
7674
|
+
var import_promises16 = __toESM(require("fs/promises"), 1);
|
|
8037
7675
|
var import_path6 = __toESM(require("path"), 1);
|
|
7676
|
+
var import_crypto4 = require("crypto");
|
|
8038
7677
|
function describeBranch(value) {
|
|
8039
7678
|
const normalized = String(value ?? "").trim();
|
|
8040
7679
|
return normalized || "(detached)";
|
|
@@ -8053,18 +7692,562 @@ function buildBranchMismatchHint(params) {
|
|
|
8053
7692
|
`Switch to ${describeBranch(params.branchName)} or rerun with ${overrideFlag} if this is intentional.`
|
|
8054
7693
|
].join("\n");
|
|
8055
7694
|
}
|
|
8056
|
-
function
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
7695
|
+
function sha256Hex(value) {
|
|
7696
|
+
return (0, import_crypto.createHash)("sha256").update(value).digest("hex");
|
|
7697
|
+
}
|
|
7698
|
+
function getCollabStateRoot() {
|
|
7699
|
+
const configured = process.env.REMIX_COLLAB_STATE_ROOT?.trim();
|
|
7700
|
+
return configured || import_path4.default.join(import_os.default.homedir(), ".remix", "collab-state");
|
|
7701
|
+
}
|
|
7702
|
+
function buildLaneStateKey(params) {
|
|
7703
|
+
const fingerprint = params.repoFingerprint?.trim();
|
|
7704
|
+
const laneId = params.laneId?.trim();
|
|
7705
|
+
const repoRoot = params.repoRoot?.trim();
|
|
7706
|
+
const stableSource = repoRoot || "unknown-repo-root";
|
|
7707
|
+
const fingerprintSource = fingerprint || "unknown-repo-fingerprint";
|
|
7708
|
+
const laneSource = laneId || "unknown-lane";
|
|
7709
|
+
return sha256Hex(`${stableSource}::${fingerprintSource}::${laneSource}`);
|
|
7710
|
+
}
|
|
7711
|
+
function getSnapshotsRoot() {
|
|
7712
|
+
return import_path4.default.join(getCollabStateRoot(), "snapshots");
|
|
7713
|
+
}
|
|
7714
|
+
function getSnapshotRecordsRoot() {
|
|
7715
|
+
return import_path4.default.join(getSnapshotsRoot(), "records");
|
|
7716
|
+
}
|
|
7717
|
+
function getSnapshotBlobsRoot() {
|
|
7718
|
+
return import_path4.default.join(getSnapshotsRoot(), "blobs");
|
|
7719
|
+
}
|
|
7720
|
+
function getBaselinesRoot() {
|
|
7721
|
+
return import_path4.default.join(getCollabStateRoot(), "baselines");
|
|
7722
|
+
}
|
|
7723
|
+
function getFinalizeQueueRoot() {
|
|
7724
|
+
return import_path4.default.join(getCollabStateRoot(), "finalize-queue");
|
|
7725
|
+
}
|
|
7726
|
+
function getBaselinePath(params) {
|
|
7727
|
+
return import_path3.default.join(getBaselinesRoot(), `${buildLaneStateKey(params)}.json`);
|
|
7728
|
+
}
|
|
7729
|
+
async function readLocalBaseline(params) {
|
|
7730
|
+
try {
|
|
7731
|
+
const raw = await import_promises14.default.readFile(getBaselinePath(params), "utf8");
|
|
7732
|
+
const parsed = JSON.parse(raw);
|
|
7733
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
7734
|
+
if (parsed.schemaVersion !== 1 || typeof parsed.key !== "string" || typeof parsed.repoRoot !== "string") {
|
|
7735
|
+
return null;
|
|
7736
|
+
}
|
|
7737
|
+
return {
|
|
7738
|
+
schemaVersion: 1,
|
|
7739
|
+
key: parsed.key,
|
|
7740
|
+
repoRoot: parsed.repoRoot,
|
|
7741
|
+
repoFingerprint: parsed.repoFingerprint ?? null,
|
|
7742
|
+
laneId: parsed.laneId ?? null,
|
|
7743
|
+
currentAppId: String(parsed.currentAppId ?? ""),
|
|
7744
|
+
branchName: parsed.branchName ?? null,
|
|
7745
|
+
lastSnapshotId: parsed.lastSnapshotId ?? null,
|
|
7746
|
+
lastSnapshotHash: parsed.lastSnapshotHash ?? null,
|
|
7747
|
+
lastServerHeadHash: parsed.lastServerHeadHash ?? null,
|
|
7748
|
+
lastSeenLocalCommitHash: parsed.lastSeenLocalCommitHash ?? null,
|
|
7749
|
+
updatedAt: String(parsed.updatedAt ?? "")
|
|
7750
|
+
};
|
|
7751
|
+
} catch {
|
|
7752
|
+
return null;
|
|
7753
|
+
}
|
|
7754
|
+
}
|
|
7755
|
+
async function writeLocalBaseline(baseline) {
|
|
7756
|
+
const key = buildLaneStateKey(baseline);
|
|
7757
|
+
const normalized = {
|
|
7758
|
+
schemaVersion: 1,
|
|
7759
|
+
key,
|
|
7760
|
+
repoRoot: baseline.repoRoot,
|
|
7761
|
+
repoFingerprint: baseline.repoFingerprint ?? null,
|
|
7762
|
+
laneId: baseline.laneId ?? null,
|
|
7763
|
+
currentAppId: baseline.currentAppId,
|
|
7764
|
+
branchName: baseline.branchName ?? null,
|
|
7765
|
+
lastSnapshotId: baseline.lastSnapshotId ?? null,
|
|
7766
|
+
lastSnapshotHash: baseline.lastSnapshotHash ?? null,
|
|
7767
|
+
lastServerHeadHash: baseline.lastServerHeadHash ?? null,
|
|
7768
|
+
lastSeenLocalCommitHash: baseline.lastSeenLocalCommitHash ?? null,
|
|
7769
|
+
updatedAt: baseline.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
7770
|
+
};
|
|
7771
|
+
await writeJsonAtomic(getBaselinePath(baseline), normalized);
|
|
7772
|
+
return normalized;
|
|
7773
|
+
}
|
|
7774
|
+
function sha256Hex2(value) {
|
|
7775
|
+
return (0, import_crypto2.createHash)("sha256").update(value).digest("hex");
|
|
7776
|
+
}
|
|
7777
|
+
function getSnapshotRecordPath(snapshotId) {
|
|
7778
|
+
return import_path5.default.join(getSnapshotRecordsRoot(), `${snapshotId}.json`);
|
|
7779
|
+
}
|
|
7780
|
+
function getBlobPath(blobHash) {
|
|
7781
|
+
return import_path5.default.join(getSnapshotBlobsRoot(), blobHash.slice(0, 2), blobHash);
|
|
7782
|
+
}
|
|
7783
|
+
async function runGitZ(args, cwd) {
|
|
7784
|
+
const res = await execa("git", args, {
|
|
7785
|
+
cwd,
|
|
7786
|
+
stderr: "ignore",
|
|
7787
|
+
stripFinalNewline: false
|
|
8067
7788
|
});
|
|
7789
|
+
return String(res.stdout || "");
|
|
7790
|
+
}
|
|
7791
|
+
async function listWorkspaceFiles(repoRoot) {
|
|
7792
|
+
const raw = await runGitZ(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--deduplicate"], repoRoot);
|
|
7793
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7794
|
+
const result = [];
|
|
7795
|
+
for (const entry of raw.split("\0")) {
|
|
7796
|
+
const relativePath = entry.trim();
|
|
7797
|
+
if (!relativePath || seen.has(relativePath)) continue;
|
|
7798
|
+
const absolutePath = import_path5.default.join(repoRoot, relativePath);
|
|
7799
|
+
try {
|
|
7800
|
+
const stat = await import_promises15.default.lstat(absolutePath);
|
|
7801
|
+
if (stat.isFile() || stat.isSymbolicLink()) {
|
|
7802
|
+
seen.add(relativePath);
|
|
7803
|
+
result.push(relativePath);
|
|
7804
|
+
}
|
|
7805
|
+
} catch {
|
|
7806
|
+
}
|
|
7807
|
+
}
|
|
7808
|
+
return result.sort((a2, b) => a2.localeCompare(b));
|
|
7809
|
+
}
|
|
7810
|
+
async function persistBlob(blobHash, content) {
|
|
7811
|
+
const blobPath = getBlobPath(blobHash);
|
|
7812
|
+
try {
|
|
7813
|
+
await import_promises15.default.access(blobPath);
|
|
7814
|
+
} catch {
|
|
7815
|
+
await import_promises15.default.mkdir(import_path5.default.dirname(blobPath), { recursive: true });
|
|
7816
|
+
if (typeof content === "string") {
|
|
7817
|
+
await import_promises15.default.writeFile(blobPath, content, "utf8");
|
|
7818
|
+
} else {
|
|
7819
|
+
await import_promises15.default.writeFile(blobPath, content);
|
|
7820
|
+
}
|
|
7821
|
+
}
|
|
7822
|
+
}
|
|
7823
|
+
function buildSnapshotHash(files) {
|
|
7824
|
+
const manifest = files.map((file) => `${file.path} ${file.mode} ${file.blobHash} ${file.size}`).join("\n");
|
|
7825
|
+
return sha256Hex2(manifest);
|
|
7826
|
+
}
|
|
7827
|
+
async function inspectLocalSnapshot(params) {
|
|
7828
|
+
const repoRoot = params.repoRoot;
|
|
7829
|
+
const files = await listWorkspaceFiles(repoRoot);
|
|
7830
|
+
const manifest = [];
|
|
7831
|
+
for (const relativePath of files) {
|
|
7832
|
+
const absolutePath = import_path5.default.join(repoRoot, relativePath);
|
|
7833
|
+
const stat = await import_promises15.default.lstat(absolutePath);
|
|
7834
|
+
if (stat.isSymbolicLink()) {
|
|
7835
|
+
const linkTarget = await import_promises15.default.readlink(absolutePath);
|
|
7836
|
+
const blobHash2 = sha256Hex2(`symlink:${linkTarget}`);
|
|
7837
|
+
if (params.persistBlobs !== false) {
|
|
7838
|
+
await persistBlob(blobHash2, linkTarget);
|
|
7839
|
+
}
|
|
7840
|
+
manifest.push({
|
|
7841
|
+
path: relativePath,
|
|
7842
|
+
mode: "symlink",
|
|
7843
|
+
blobHash: blobHash2,
|
|
7844
|
+
size: Buffer.byteLength(linkTarget)
|
|
7845
|
+
});
|
|
7846
|
+
continue;
|
|
7847
|
+
}
|
|
7848
|
+
const content = await import_promises15.default.readFile(absolutePath);
|
|
7849
|
+
const blobHash = sha256Hex2(content);
|
|
7850
|
+
if (params.persistBlobs !== false) {
|
|
7851
|
+
await persistBlob(blobHash, content);
|
|
7852
|
+
}
|
|
7853
|
+
manifest.push({
|
|
7854
|
+
path: relativePath,
|
|
7855
|
+
mode: stat.mode & 73 ? "executable" : "file",
|
|
7856
|
+
blobHash,
|
|
7857
|
+
size: stat.size
|
|
7858
|
+
});
|
|
7859
|
+
}
|
|
7860
|
+
const normalizedManifest = manifest.sort((a2, b) => a2.path.localeCompare(b.path));
|
|
7861
|
+
return {
|
|
7862
|
+
repoRoot,
|
|
7863
|
+
repoFingerprint: params.repoFingerprint ?? null,
|
|
7864
|
+
laneId: params.laneId ?? null,
|
|
7865
|
+
branchName: params.branchName ?? await getCurrentBranch(repoRoot).catch(() => null),
|
|
7866
|
+
localCommitHash: await getHeadCommitHash(repoRoot).catch(() => null),
|
|
7867
|
+
snapshotHash: buildSnapshotHash(normalizedManifest),
|
|
7868
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7869
|
+
files: normalizedManifest
|
|
7870
|
+
};
|
|
7871
|
+
}
|
|
7872
|
+
async function captureLocalSnapshot(params) {
|
|
7873
|
+
const inspection = await inspectLocalSnapshot({ ...params, persistBlobs: true });
|
|
7874
|
+
const snapshot = {
|
|
7875
|
+
schemaVersion: 1,
|
|
7876
|
+
id: (0, import_crypto2.randomUUID)(),
|
|
7877
|
+
...inspection
|
|
7878
|
+
};
|
|
7879
|
+
await writeJsonAtomic(getSnapshotRecordPath(snapshot.id), snapshot);
|
|
7880
|
+
return snapshot;
|
|
7881
|
+
}
|
|
7882
|
+
async function readLocalSnapshot(snapshotId) {
|
|
7883
|
+
if (!snapshotId) return null;
|
|
7884
|
+
try {
|
|
7885
|
+
const raw = await import_promises15.default.readFile(getSnapshotRecordPath(snapshotId), "utf8");
|
|
7886
|
+
const parsed = JSON.parse(raw);
|
|
7887
|
+
if (!parsed || parsed.schemaVersion !== 1) return null;
|
|
7888
|
+
return parsed;
|
|
7889
|
+
} catch {
|
|
7890
|
+
return null;
|
|
7891
|
+
}
|
|
7892
|
+
}
|
|
7893
|
+
async function materializeLocalSnapshot(snapshotId, targetDir) {
|
|
7894
|
+
const snapshot = await readLocalSnapshot(snapshotId);
|
|
7895
|
+
await import_promises15.default.mkdir(targetDir, { recursive: true });
|
|
7896
|
+
if (!snapshot) return;
|
|
7897
|
+
for (const entry of snapshot.files) {
|
|
7898
|
+
const destination = import_path5.default.join(targetDir, entry.path);
|
|
7899
|
+
await import_promises15.default.mkdir(import_path5.default.dirname(destination), { recursive: true });
|
|
7900
|
+
const blobPath = getBlobPath(entry.blobHash);
|
|
7901
|
+
if (entry.mode === "symlink") {
|
|
7902
|
+
const linkTarget = await import_promises15.default.readFile(blobPath, "utf8");
|
|
7903
|
+
await import_promises15.default.symlink(linkTarget, destination);
|
|
7904
|
+
continue;
|
|
7905
|
+
}
|
|
7906
|
+
await import_promises15.default.copyFile(blobPath, destination);
|
|
7907
|
+
if (entry.mode === "executable") {
|
|
7908
|
+
await import_promises15.default.chmod(destination, 493);
|
|
7909
|
+
}
|
|
7910
|
+
}
|
|
7911
|
+
}
|
|
7912
|
+
async function clearDirectoryExceptGit(targetDir) {
|
|
7913
|
+
const entries = await import_promises15.default.readdir(targetDir, { withFileTypes: true });
|
|
7914
|
+
for (const entry of entries) {
|
|
7915
|
+
if (entry.name === ".git") continue;
|
|
7916
|
+
await import_promises15.default.rm(import_path5.default.join(targetDir, entry.name), { recursive: true, force: true });
|
|
7917
|
+
}
|
|
7918
|
+
}
|
|
7919
|
+
async function diffLocalSnapshots(params) {
|
|
7920
|
+
const tempRoot = await import_promises15.default.mkdtemp(import_path5.default.join(import_os2.default.tmpdir(), "remix-snapshot-diff-"));
|
|
7921
|
+
const repoDir = import_path5.default.join(tempRoot, "repo");
|
|
7922
|
+
await import_promises15.default.mkdir(repoDir, { recursive: true });
|
|
7923
|
+
try {
|
|
7924
|
+
await materializeLocalSnapshot(params.baseSnapshotId, repoDir);
|
|
7925
|
+
await execa("git", ["init"], { cwd: repoDir, stderr: "ignore" });
|
|
7926
|
+
await execa("git", ["add", "-A"], { cwd: repoDir, stderr: "ignore" });
|
|
7927
|
+
await execa(
|
|
7928
|
+
"git",
|
|
7929
|
+
["-c", "user.name=Remix", "-c", "user.email=remix@local", "commit", "--allow-empty", "-m", "baseline snapshot"],
|
|
7930
|
+
{ cwd: repoDir, stderr: "ignore" }
|
|
7931
|
+
);
|
|
7932
|
+
await clearDirectoryExceptGit(repoDir);
|
|
7933
|
+
await materializeLocalSnapshot(params.targetSnapshotId, repoDir);
|
|
7934
|
+
await execa("git", ["add", "-A"], { cwd: repoDir, stderr: "ignore" });
|
|
7935
|
+
const diffRes = await execa("git", ["diff", "--binary", "--no-ext-diff", "--cached", "HEAD"], {
|
|
7936
|
+
cwd: repoDir,
|
|
7937
|
+
reject: false,
|
|
7938
|
+
stderr: "ignore",
|
|
7939
|
+
stripFinalNewline: false
|
|
7940
|
+
});
|
|
7941
|
+
const pathsRes = await execa("git", ["diff", "--name-only", "--cached", "HEAD", "-z"], {
|
|
7942
|
+
cwd: repoDir,
|
|
7943
|
+
reject: false,
|
|
7944
|
+
stderr: "ignore",
|
|
7945
|
+
stripFinalNewline: false
|
|
7946
|
+
});
|
|
7947
|
+
const diff = String(diffRes.stdout || "");
|
|
7948
|
+
const changedPaths = String(pathsRes.stdout || "").split("\0").map((value) => value.trim()).filter(Boolean);
|
|
7949
|
+
return {
|
|
7950
|
+
baseSnapshotId: params.baseSnapshotId,
|
|
7951
|
+
targetSnapshotId: params.targetSnapshotId,
|
|
7952
|
+
diff,
|
|
7953
|
+
diffSha256: diff ? sha256Hex2(diff) : null,
|
|
7954
|
+
changedPaths,
|
|
7955
|
+
stats: summarizeUnifiedDiff(diff)
|
|
7956
|
+
};
|
|
7957
|
+
} finally {
|
|
7958
|
+
await import_promises15.default.rm(tempRoot, { recursive: true, force: true });
|
|
7959
|
+
}
|
|
7960
|
+
}
|
|
7961
|
+
var FINALIZE_JOB_LOCK_STALE_MS = 10 * 60 * 1e3;
|
|
7962
|
+
var TERMINAL_FINALIZE_JOB_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
7963
|
+
function getJobPath(id) {
|
|
7964
|
+
return import_path6.default.join(getFinalizeQueueRoot(), `${id}.json`);
|
|
7965
|
+
}
|
|
7966
|
+
function getJobLockPath(id) {
|
|
7967
|
+
return import_path6.default.join(getFinalizeQueueRoot(), `${id}.lock`);
|
|
7968
|
+
}
|
|
7969
|
+
function isPastDue(isoTimestamp) {
|
|
7970
|
+
if (!isoTimestamp) return true;
|
|
7971
|
+
const parsed = Date.parse(isoTimestamp);
|
|
7972
|
+
return Number.isFinite(parsed) && parsed <= Date.now();
|
|
7973
|
+
}
|
|
7974
|
+
function isStaleAttempt(job) {
|
|
7975
|
+
if (job.status !== "processing") return false;
|
|
7976
|
+
if (!job.lastAttemptAt) return true;
|
|
7977
|
+
const parsed = Date.parse(job.lastAttemptAt);
|
|
7978
|
+
if (!Number.isFinite(parsed)) return true;
|
|
7979
|
+
return Date.now() - parsed >= FINALIZE_JOB_LOCK_STALE_MS;
|
|
7980
|
+
}
|
|
7981
|
+
function readMetadataDisposition(job) {
|
|
7982
|
+
const value = job.metadata.failureDisposition;
|
|
7983
|
+
return value === "retryable" || value === "terminal" ? value : null;
|
|
7984
|
+
}
|
|
7985
|
+
function isTerminalFailure(job) {
|
|
7986
|
+
return job.status === "failed" && readMetadataDisposition(job) === "terminal";
|
|
7987
|
+
}
|
|
7988
|
+
function isTerminalFailureExpired(job) {
|
|
7989
|
+
if (!isTerminalFailure(job)) return false;
|
|
7990
|
+
const updatedAtMs = Date.parse(job.updatedAt);
|
|
7991
|
+
if (!Number.isFinite(updatedAtMs)) return false;
|
|
7992
|
+
return Date.now() - updatedAtMs >= TERMINAL_FINALIZE_JOB_RETENTION_MS;
|
|
7993
|
+
}
|
|
7994
|
+
function matchesJobScope(job, scope) {
|
|
7995
|
+
if (job.repoRoot !== scope.repoRoot) return false;
|
|
7996
|
+
if (scope.currentAppId && job.currentAppId !== scope.currentAppId) return false;
|
|
7997
|
+
if (scope.laneId && job.laneId !== scope.laneId) return false;
|
|
7998
|
+
if (scope.repoFingerprint && job.repoFingerprint && job.repoFingerprint !== scope.repoFingerprint) return false;
|
|
7999
|
+
return true;
|
|
8000
|
+
}
|
|
8001
|
+
function createEmptyPendingFinalizeQueueSummary() {
|
|
8002
|
+
return {
|
|
8003
|
+
state: "idle",
|
|
8004
|
+
activeJobCount: 0,
|
|
8005
|
+
queuedJobCount: 0,
|
|
8006
|
+
processingJobCount: 0,
|
|
8007
|
+
retryScheduledJobCount: 0,
|
|
8008
|
+
failedJobCount: 0,
|
|
8009
|
+
oldestCapturedAt: null,
|
|
8010
|
+
newestCapturedAt: null,
|
|
8011
|
+
nextRetryAt: null,
|
|
8012
|
+
latestError: null
|
|
8013
|
+
};
|
|
8014
|
+
}
|
|
8015
|
+
async function acquireJobLock(jobId) {
|
|
8016
|
+
const lockPath = getJobLockPath(jobId);
|
|
8017
|
+
try {
|
|
8018
|
+
await import_promises16.default.mkdir(lockPath);
|
|
8019
|
+
return true;
|
|
8020
|
+
} catch (error) {
|
|
8021
|
+
if (error?.code !== "EEXIST") {
|
|
8022
|
+
throw error;
|
|
8023
|
+
}
|
|
8024
|
+
}
|
|
8025
|
+
try {
|
|
8026
|
+
const stat = await import_promises16.default.stat(lockPath);
|
|
8027
|
+
if (Date.now() - stat.mtimeMs < FINALIZE_JOB_LOCK_STALE_MS) {
|
|
8028
|
+
return false;
|
|
8029
|
+
}
|
|
8030
|
+
await import_promises16.default.rm(lockPath, { recursive: true, force: true });
|
|
8031
|
+
} catch (error) {
|
|
8032
|
+
if (error?.code !== "ENOENT") {
|
|
8033
|
+
throw error;
|
|
8034
|
+
}
|
|
8035
|
+
}
|
|
8036
|
+
try {
|
|
8037
|
+
await import_promises16.default.mkdir(lockPath);
|
|
8038
|
+
return true;
|
|
8039
|
+
} catch (error) {
|
|
8040
|
+
if (error?.code === "EEXIST") {
|
|
8041
|
+
return false;
|
|
8042
|
+
}
|
|
8043
|
+
throw error;
|
|
8044
|
+
}
|
|
8045
|
+
}
|
|
8046
|
+
function normalizeJob(input) {
|
|
8047
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8048
|
+
return {
|
|
8049
|
+
schemaVersion: 1,
|
|
8050
|
+
id: input.id ?? (0, import_crypto3.randomUUID)(),
|
|
8051
|
+
status: input.status,
|
|
8052
|
+
repoRoot: input.repoRoot,
|
|
8053
|
+
repoFingerprint: input.repoFingerprint ?? null,
|
|
8054
|
+
currentAppId: input.currentAppId,
|
|
8055
|
+
laneId: input.laneId ?? null,
|
|
8056
|
+
threadId: input.threadId ?? null,
|
|
8057
|
+
branchName: input.branchName ?? null,
|
|
8058
|
+
prompt: input.prompt,
|
|
8059
|
+
assistantResponse: input.assistantResponse,
|
|
8060
|
+
baselineSnapshotId: input.baselineSnapshotId ?? null,
|
|
8061
|
+
baselineServerHeadHash: input.baselineServerHeadHash ?? null,
|
|
8062
|
+
currentSnapshotId: input.currentSnapshotId,
|
|
8063
|
+
capturedAt: input.capturedAt ?? now,
|
|
8064
|
+
updatedAt: input.updatedAt ?? now,
|
|
8065
|
+
idempotencyKey: input.idempotencyKey ?? null,
|
|
8066
|
+
error: input.error ?? null,
|
|
8067
|
+
retryCount: Number.isFinite(input.retryCount) ? Math.max(0, Number(input.retryCount)) : 0,
|
|
8068
|
+
lastAttemptAt: input.lastAttemptAt ?? null,
|
|
8069
|
+
nextRetryAt: input.nextRetryAt ?? null,
|
|
8070
|
+
metadata: input.metadata ?? {}
|
|
8071
|
+
};
|
|
8072
|
+
}
|
|
8073
|
+
async function enqueuePendingFinalizeJob(input) {
|
|
8074
|
+
const job = normalizeJob(input);
|
|
8075
|
+
await writeJsonAtomic(getJobPath(job.id), job);
|
|
8076
|
+
return job;
|
|
8077
|
+
}
|
|
8078
|
+
async function readPendingFinalizeJob(jobId) {
|
|
8079
|
+
try {
|
|
8080
|
+
const raw = await import_promises16.default.readFile(getJobPath(jobId), "utf8");
|
|
8081
|
+
const parsed = JSON.parse(raw);
|
|
8082
|
+
if (!parsed || parsed.schemaVersion !== 1 || typeof parsed.id !== "string") return null;
|
|
8083
|
+
return normalizeJob({
|
|
8084
|
+
id: parsed.id,
|
|
8085
|
+
status: parsed.status ?? "queued",
|
|
8086
|
+
repoRoot: String(parsed.repoRoot ?? ""),
|
|
8087
|
+
repoFingerprint: parsed.repoFingerprint ?? null,
|
|
8088
|
+
currentAppId: String(parsed.currentAppId ?? ""),
|
|
8089
|
+
laneId: parsed.laneId ?? null,
|
|
8090
|
+
threadId: parsed.threadId ?? null,
|
|
8091
|
+
branchName: parsed.branchName ?? null,
|
|
8092
|
+
prompt: String(parsed.prompt ?? ""),
|
|
8093
|
+
assistantResponse: String(parsed.assistantResponse ?? ""),
|
|
8094
|
+
baselineSnapshotId: parsed.baselineSnapshotId ?? null,
|
|
8095
|
+
baselineServerHeadHash: parsed.baselineServerHeadHash ?? null,
|
|
8096
|
+
currentSnapshotId: String(parsed.currentSnapshotId ?? ""),
|
|
8097
|
+
capturedAt: parsed.capturedAt,
|
|
8098
|
+
updatedAt: parsed.updatedAt,
|
|
8099
|
+
idempotencyKey: parsed.idempotencyKey ?? null,
|
|
8100
|
+
error: parsed.error ?? null,
|
|
8101
|
+
retryCount: parsed.retryCount ?? 0,
|
|
8102
|
+
lastAttemptAt: parsed.lastAttemptAt ?? null,
|
|
8103
|
+
nextRetryAt: parsed.nextRetryAt ?? null,
|
|
8104
|
+
metadata: parsed.metadata ?? {}
|
|
8105
|
+
});
|
|
8106
|
+
} catch {
|
|
8107
|
+
return null;
|
|
8108
|
+
}
|
|
8109
|
+
}
|
|
8110
|
+
async function listPendingFinalizeJobs() {
|
|
8111
|
+
try {
|
|
8112
|
+
const entries = await import_promises16.default.readdir(getFinalizeQueueRoot(), { withFileTypes: true });
|
|
8113
|
+
const jobs = await Promise.all(
|
|
8114
|
+
entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => readPendingFinalizeJob(entry.name.replace(/\.json$/, "")))
|
|
8115
|
+
);
|
|
8116
|
+
return jobs.filter((job) => Boolean(job)).sort((a2, b) => a2.capturedAt.localeCompare(b.capturedAt));
|
|
8117
|
+
} catch (error) {
|
|
8118
|
+
if (error?.code === "ENOENT") {
|
|
8119
|
+
return [];
|
|
8120
|
+
}
|
|
8121
|
+
throw error;
|
|
8122
|
+
}
|
|
8123
|
+
}
|
|
8124
|
+
async function prunePendingFinalizeJobs() {
|
|
8125
|
+
const jobs = await listPendingFinalizeJobs();
|
|
8126
|
+
await Promise.all(
|
|
8127
|
+
jobs.filter((job) => job.status === "completed" || isTerminalFailureExpired(job)).map((job) => removePendingFinalizeJob(job.id))
|
|
8128
|
+
);
|
|
8129
|
+
}
|
|
8130
|
+
async function summarizePendingFinalizeJobs(scope) {
|
|
8131
|
+
const jobs = (await listPendingFinalizeJobs()).filter((job) => matchesJobScope(job, scope));
|
|
8132
|
+
const summary = createEmptyPendingFinalizeQueueSummary();
|
|
8133
|
+
const relevantJobs = jobs.filter((job) => job.status !== "completed");
|
|
8134
|
+
if (relevantJobs.length === 0) return summary;
|
|
8135
|
+
summary.oldestCapturedAt = relevantJobs[0]?.capturedAt ?? null;
|
|
8136
|
+
summary.newestCapturedAt = relevantJobs[relevantJobs.length - 1]?.capturedAt ?? null;
|
|
8137
|
+
for (const job of relevantJobs) {
|
|
8138
|
+
if (job.error) {
|
|
8139
|
+
summary.latestError = job.error;
|
|
8140
|
+
}
|
|
8141
|
+
if (job.nextRetryAt && (!summary.nextRetryAt || job.nextRetryAt < summary.nextRetryAt)) {
|
|
8142
|
+
summary.nextRetryAt = job.nextRetryAt;
|
|
8143
|
+
}
|
|
8144
|
+
if (job.status === "processing") {
|
|
8145
|
+
summary.processingJobCount += 1;
|
|
8146
|
+
continue;
|
|
8147
|
+
}
|
|
8148
|
+
if (job.status === "failed") {
|
|
8149
|
+
summary.failedJobCount += 1;
|
|
8150
|
+
continue;
|
|
8151
|
+
}
|
|
8152
|
+
if (!isPastDue(job.nextRetryAt)) {
|
|
8153
|
+
summary.retryScheduledJobCount += 1;
|
|
8154
|
+
continue;
|
|
8155
|
+
}
|
|
8156
|
+
summary.queuedJobCount += 1;
|
|
8157
|
+
}
|
|
8158
|
+
summary.activeJobCount = summary.queuedJobCount + summary.processingJobCount + summary.retryScheduledJobCount;
|
|
8159
|
+
if (summary.processingJobCount > 0) {
|
|
8160
|
+
summary.state = "processing";
|
|
8161
|
+
} else if (summary.queuedJobCount > 0) {
|
|
8162
|
+
summary.state = "queued";
|
|
8163
|
+
} else if (summary.retryScheduledJobCount > 0) {
|
|
8164
|
+
summary.state = "retry_scheduled";
|
|
8165
|
+
} else if (summary.failedJobCount > 0) {
|
|
8166
|
+
summary.state = "failed";
|
|
8167
|
+
}
|
|
8168
|
+
return summary;
|
|
8169
|
+
}
|
|
8170
|
+
async function updatePendingFinalizeJob(jobId, update) {
|
|
8171
|
+
const existing = await readPendingFinalizeJob(jobId);
|
|
8172
|
+
if (!existing) return null;
|
|
8173
|
+
const next = {
|
|
8174
|
+
...existing,
|
|
8175
|
+
...update,
|
|
8176
|
+
schemaVersion: 1,
|
|
8177
|
+
id: existing.id,
|
|
8178
|
+
capturedAt: existing.capturedAt,
|
|
8179
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8180
|
+
metadata: update.metadata ? { ...existing.metadata, ...update.metadata } : existing.metadata
|
|
8181
|
+
};
|
|
8182
|
+
await writeJsonAtomic(getJobPath(jobId), next);
|
|
8183
|
+
return next;
|
|
8184
|
+
}
|
|
8185
|
+
async function claimPendingFinalizeJob(jobId) {
|
|
8186
|
+
const lockPath = getJobLockPath(jobId);
|
|
8187
|
+
const lockAcquired = await acquireJobLock(jobId);
|
|
8188
|
+
if (!lockAcquired) return null;
|
|
8189
|
+
let released = false;
|
|
8190
|
+
const release = async () => {
|
|
8191
|
+
if (released) return;
|
|
8192
|
+
released = true;
|
|
8193
|
+
await import_promises16.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
|
|
8194
|
+
};
|
|
8195
|
+
try {
|
|
8196
|
+
let existing = await readPendingFinalizeJob(jobId);
|
|
8197
|
+
if (!existing) {
|
|
8198
|
+
await release();
|
|
8199
|
+
return null;
|
|
8200
|
+
}
|
|
8201
|
+
if (isStaleAttempt(existing)) {
|
|
8202
|
+
const recovered = await updatePendingFinalizeJob(jobId, {
|
|
8203
|
+
status: "queued",
|
|
8204
|
+
error: existing.error ?? "Recovered a stale finalize processing lease.",
|
|
8205
|
+
nextRetryAt: null
|
|
8206
|
+
});
|
|
8207
|
+
existing = recovered ?? existing;
|
|
8208
|
+
}
|
|
8209
|
+
if (existing.status === "failed") {
|
|
8210
|
+
if (isTerminalFailure(existing)) {
|
|
8211
|
+
await release();
|
|
8212
|
+
return null;
|
|
8213
|
+
}
|
|
8214
|
+
const recovered = await updatePendingFinalizeJob(jobId, {
|
|
8215
|
+
status: "queued",
|
|
8216
|
+
nextRetryAt: existing.nextRetryAt ?? null
|
|
8217
|
+
});
|
|
8218
|
+
existing = recovered ?? existing;
|
|
8219
|
+
}
|
|
8220
|
+
if (existing.status !== "queued" || !isPastDue(existing.nextRetryAt)) {
|
|
8221
|
+
await release();
|
|
8222
|
+
return null;
|
|
8223
|
+
}
|
|
8224
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8225
|
+
const claimed = await updatePendingFinalizeJob(jobId, {
|
|
8226
|
+
status: "processing",
|
|
8227
|
+
error: null,
|
|
8228
|
+
retryCount: existing.retryCount + 1,
|
|
8229
|
+
lastAttemptAt: now,
|
|
8230
|
+
nextRetryAt: null
|
|
8231
|
+
});
|
|
8232
|
+
if (!claimed) {
|
|
8233
|
+
await release();
|
|
8234
|
+
return null;
|
|
8235
|
+
}
|
|
8236
|
+
return { job: claimed, release };
|
|
8237
|
+
} catch (error) {
|
|
8238
|
+
await release();
|
|
8239
|
+
throw error;
|
|
8240
|
+
}
|
|
8241
|
+
}
|
|
8242
|
+
async function removePendingFinalizeJob(jobId) {
|
|
8243
|
+
try {
|
|
8244
|
+
await import_promises16.default.unlink(getJobPath(jobId));
|
|
8245
|
+
} catch (error) {
|
|
8246
|
+
if (error?.code !== "ENOENT") {
|
|
8247
|
+
throw error;
|
|
8248
|
+
}
|
|
8249
|
+
}
|
|
8250
|
+
await import_promises16.default.rm(getJobLockPath(jobId), { recursive: true, force: true }).catch(() => void 0);
|
|
8068
8251
|
}
|
|
8069
8252
|
function unwrapResponseObject(resp, label) {
|
|
8070
8253
|
const obj = resp?.responseObject;
|
|
@@ -8078,7 +8261,7 @@ function sleep(ms) {
|
|
|
8078
8261
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8079
8262
|
}
|
|
8080
8263
|
function buildDeterministicIdempotencyKey(parts) {
|
|
8081
|
-
return (0,
|
|
8264
|
+
return (0, import_crypto4.createHash)("sha256").update(JSON.stringify(parts)).digest("hex");
|
|
8082
8265
|
}
|
|
8083
8266
|
function formatCliErrorDetail(err) {
|
|
8084
8267
|
if (err instanceof RemixError) {
|
|
@@ -8089,25 +8272,6 @@ function formatCliErrorDetail(err) {
|
|
|
8089
8272
|
}
|
|
8090
8273
|
return typeof err === "string" && err.trim() ? err.trim() : null;
|
|
8091
8274
|
}
|
|
8092
|
-
async function pollAppReady(api, appId) {
|
|
8093
|
-
const started = Date.now();
|
|
8094
|
-
let delay = 2e3;
|
|
8095
|
-
while (Date.now() - started < 20 * 60 * 1e3) {
|
|
8096
|
-
const appResp = await api.getApp(appId);
|
|
8097
|
-
const app = unwrapResponseObject(appResp, "app");
|
|
8098
|
-
const status = typeof app.status === "string" ? app.status : "";
|
|
8099
|
-
if (status === "ready") return app;
|
|
8100
|
-
if (status === "error") {
|
|
8101
|
-
throw new RemixError("App is in error state.", {
|
|
8102
|
-
exitCode: 1,
|
|
8103
|
-
hint: typeof app.statusError === "string" ? app.statusError : null
|
|
8104
|
-
});
|
|
8105
|
-
}
|
|
8106
|
-
await sleep(delay);
|
|
8107
|
-
delay = Math.min(1e4, Math.floor(delay * 1.4));
|
|
8108
|
-
}
|
|
8109
|
-
throw new RemixError("Timed out waiting for app to become ready.", { exitCode: 1 });
|
|
8110
|
-
}
|
|
8111
8275
|
async function pollChangeStep(api, appId, changeStepId) {
|
|
8112
8276
|
const started = Date.now();
|
|
8113
8277
|
let delay = 1500;
|
|
@@ -8157,6 +8321,10 @@ function normalizeBranchName2(value) {
|
|
|
8157
8321
|
}
|
|
8158
8322
|
function buildBindingFromLane(state, lane) {
|
|
8159
8323
|
if (!lane.currentAppId || !lane.upstreamAppId) return null;
|
|
8324
|
+
const resolvedBranch = normalizeBranchName2(lane.branchName) ?? state.currentBranch ?? null;
|
|
8325
|
+
const resolvedDefaultBranch = normalizeBranchName2(lane.defaultBranch) ?? normalizeBranchName2(state.defaultBranch);
|
|
8326
|
+
const explicitRootProjectId = state.explicitRootBinding?.projectId ?? null;
|
|
8327
|
+
const bindingMode = explicitRootProjectId && lane.projectId === explicitRootProjectId && resolvedBranch && resolvedBranch === resolvedDefaultBranch ? "explicit_root" : "lane";
|
|
8160
8328
|
return {
|
|
8161
8329
|
schemaVersion: 3,
|
|
8162
8330
|
projectId: lane.projectId ?? state.projectId,
|
|
@@ -8167,8 +8335,8 @@ function buildBindingFromLane(state, lane) {
|
|
|
8167
8335
|
remoteUrl: lane.remoteUrl ?? state.remoteUrl ?? null,
|
|
8168
8336
|
defaultBranch: lane.defaultBranch ?? state.defaultBranch ?? null,
|
|
8169
8337
|
laneId: lane.laneId ?? null,
|
|
8170
|
-
branchName:
|
|
8171
|
-
bindingMode
|
|
8338
|
+
branchName: resolvedBranch,
|
|
8339
|
+
bindingMode
|
|
8172
8340
|
};
|
|
8173
8341
|
}
|
|
8174
8342
|
function shouldPersistRemoteLaneMetadata(localBinding, lane) {
|
|
@@ -8197,6 +8365,16 @@ async function persistResolvedLane(repoRoot, binding) {
|
|
|
8197
8365
|
});
|
|
8198
8366
|
return readCollabBinding(repoRoot);
|
|
8199
8367
|
}
|
|
8368
|
+
function buildAmbiguousResolution(params) {
|
|
8369
|
+
return {
|
|
8370
|
+
status: "ambiguous_family_selection",
|
|
8371
|
+
currentBranch: params.currentBranch,
|
|
8372
|
+
projectIds: Array.isArray(params.lane.projectIds) ? params.lane.projectIds.filter((value) => typeof value === "string" && value.trim().length > 0) : [],
|
|
8373
|
+
repoFingerprint: params.lane.repoFingerprint ?? params.state.repoFingerprint,
|
|
8374
|
+
remoteUrl: params.lane.remoteUrl ?? params.state.remoteUrl,
|
|
8375
|
+
defaultBranch: params.lane.defaultBranch ?? params.state.defaultBranch
|
|
8376
|
+
};
|
|
8377
|
+
}
|
|
8200
8378
|
async function resolveActiveLaneBinding(params) {
|
|
8201
8379
|
const state = await readCollabBindingState(params.repoRoot);
|
|
8202
8380
|
if (!state) {
|
|
@@ -8219,13 +8397,16 @@ async function resolveActiveLaneBinding(params) {
|
|
|
8219
8397
|
};
|
|
8220
8398
|
}
|
|
8221
8399
|
const laneResp2 = await params.api.resolveProjectLaneBinding({
|
|
8222
|
-
projectId: localBinding.projectId ?? state.projectId ?? void 0,
|
|
8400
|
+
projectId: state.explicitRootBinding?.projectId ?? (requireRemoteLane ? void 0 : localBinding.projectId ?? state.projectId ?? void 0),
|
|
8223
8401
|
repoFingerprint: state.repoFingerprint ?? void 0,
|
|
8224
8402
|
remoteUrl: state.remoteUrl ?? void 0,
|
|
8225
8403
|
defaultBranch: state.defaultBranch ?? void 0,
|
|
8226
8404
|
branchName: currentBranch
|
|
8227
8405
|
});
|
|
8228
8406
|
const lane2 = unwrapResponseObject(laneResp2, "project lane binding");
|
|
8407
|
+
if (lane2.status === "ambiguous_family_selection") {
|
|
8408
|
+
return buildAmbiguousResolution({ state, currentBranch, lane: lane2 });
|
|
8409
|
+
}
|
|
8229
8410
|
if (lane2.status === "resolved") {
|
|
8230
8411
|
const resolvedBranch = normalizeBranchName2(lane2.branchName);
|
|
8231
8412
|
const resolvedProjectId = lane2.projectId ?? state.projectId;
|
|
@@ -8268,12 +8449,12 @@ async function resolveActiveLaneBinding(params) {
|
|
|
8268
8449
|
return {
|
|
8269
8450
|
status: "missing_branch_binding",
|
|
8270
8451
|
currentBranch,
|
|
8271
|
-
projectId: state.projectId,
|
|
8272
|
-
repoFingerprint: state.repoFingerprint,
|
|
8273
|
-
remoteUrl: state.remoteUrl,
|
|
8274
|
-
defaultBranch: state.defaultBranch,
|
|
8275
|
-
upstreamAppId: localBinding.upstreamAppId ?? null,
|
|
8276
|
-
threadId: localBinding.threadId ?? null
|
|
8452
|
+
projectId: lane2.projectId ?? state.projectId,
|
|
8453
|
+
repoFingerprint: lane2.repoFingerprint ?? state.repoFingerprint,
|
|
8454
|
+
remoteUrl: lane2.remoteUrl ?? state.remoteUrl,
|
|
8455
|
+
defaultBranch: lane2.defaultBranch ?? state.defaultBranch,
|
|
8456
|
+
upstreamAppId: lane2.upstreamAppId ?? localBinding.upstreamAppId ?? null,
|
|
8457
|
+
threadId: lane2.threadId ?? localBinding.threadId ?? null
|
|
8277
8458
|
};
|
|
8278
8459
|
}
|
|
8279
8460
|
return {
|
|
@@ -8296,13 +8477,16 @@ async function resolveActiveLaneBinding(params) {
|
|
|
8296
8477
|
};
|
|
8297
8478
|
}
|
|
8298
8479
|
const laneResp = await params.api.resolveProjectLaneBinding({
|
|
8299
|
-
projectId: state.projectId ?? void 0,
|
|
8480
|
+
projectId: state.explicitRootBinding?.projectId ?? state.projectId ?? void 0,
|
|
8300
8481
|
repoFingerprint: state.repoFingerprint ?? void 0,
|
|
8301
8482
|
remoteUrl: state.remoteUrl ?? void 0,
|
|
8302
8483
|
defaultBranch: state.defaultBranch ?? void 0,
|
|
8303
8484
|
branchName: currentBranch
|
|
8304
8485
|
});
|
|
8305
8486
|
const lane = unwrapResponseObject(laneResp, "project lane binding");
|
|
8487
|
+
if (lane.status === "ambiguous_family_selection") {
|
|
8488
|
+
return buildAmbiguousResolution({ state, currentBranch, lane });
|
|
8489
|
+
}
|
|
8306
8490
|
if (lane.status === "resolved") {
|
|
8307
8491
|
const binding = buildBindingFromLane(state, lane);
|
|
8308
8492
|
if (binding) {
|
|
@@ -8314,18 +8498,15 @@ async function resolveActiveLaneBinding(params) {
|
|
|
8314
8498
|
};
|
|
8315
8499
|
}
|
|
8316
8500
|
}
|
|
8317
|
-
if (lane.status === "binding_not_found") {
|
|
8318
|
-
return { status: "not_bound", currentBranch };
|
|
8319
|
-
}
|
|
8320
8501
|
return {
|
|
8321
8502
|
status: "missing_branch_binding",
|
|
8322
8503
|
currentBranch,
|
|
8323
|
-
projectId: lane.projectId ?? state.projectId,
|
|
8504
|
+
projectId: lane.projectId ?? state.explicitRootBinding?.projectId ?? state.projectId,
|
|
8324
8505
|
repoFingerprint: lane.repoFingerprint ?? state.repoFingerprint,
|
|
8325
8506
|
remoteUrl: lane.remoteUrl ?? state.remoteUrl,
|
|
8326
8507
|
defaultBranch: lane.defaultBranch ?? state.defaultBranch,
|
|
8327
|
-
upstreamAppId: lane.upstreamAppId ?? null,
|
|
8328
|
-
threadId: lane.threadId ?? null
|
|
8508
|
+
upstreamAppId: lane.upstreamAppId ?? state.explicitRootBinding?.upstreamAppId ?? null,
|
|
8509
|
+
threadId: lane.threadId ?? state.explicitRootBinding?.threadId ?? null
|
|
8329
8510
|
};
|
|
8330
8511
|
}
|
|
8331
8512
|
async function ensureActiveLaneBinding(params) {
|
|
@@ -8345,6 +8526,12 @@ async function ensureActiveLaneBinding(params) {
|
|
|
8345
8526
|
hint: `Local app ${resolved.binding.currentAppId}; server app ${resolved.resolvedLane.currentAppId ?? "(unknown)"}. Repair the branch binding before running ${params.operation ?? "this command"}.`
|
|
8346
8527
|
});
|
|
8347
8528
|
}
|
|
8529
|
+
if (resolved.status === "ambiguous_family_selection") {
|
|
8530
|
+
throw new RemixError("Multiple canonical Remix families match this repository.", {
|
|
8531
|
+
exitCode: 2,
|
|
8532
|
+
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."
|
|
8533
|
+
});
|
|
8534
|
+
}
|
|
8348
8535
|
if (resolved.status === "not_bound") {
|
|
8349
8536
|
return null;
|
|
8350
8537
|
}
|
|
@@ -8359,614 +8546,567 @@ async function ensureActiveLaneBinding(params) {
|
|
|
8359
8546
|
hint: `Run \`remix collab init\` on branch ${resolved.currentBranch} before running ${params.operation ?? "this command"}.`
|
|
8360
8547
|
});
|
|
8361
8548
|
}
|
|
8362
|
-
|
|
8549
|
+
function buildBaseState() {
|
|
8550
|
+
return {
|
|
8551
|
+
status: "ready",
|
|
8552
|
+
repoState: null,
|
|
8553
|
+
repoRoot: null,
|
|
8554
|
+
binding: null,
|
|
8555
|
+
currentBranch: null,
|
|
8556
|
+
branchName: null,
|
|
8557
|
+
localCommitHash: null,
|
|
8558
|
+
currentSnapshotHash: null,
|
|
8559
|
+
currentServerHeadHash: null,
|
|
8560
|
+
currentServerHeadCommitId: null,
|
|
8561
|
+
worktreeClean: false,
|
|
8562
|
+
pendingFinalize: {
|
|
8563
|
+
state: "idle",
|
|
8564
|
+
activeJobCount: 0,
|
|
8565
|
+
queuedJobCount: 0,
|
|
8566
|
+
processingJobCount: 0,
|
|
8567
|
+
retryScheduledJobCount: 0,
|
|
8568
|
+
failedJobCount: 0,
|
|
8569
|
+
oldestCapturedAt: null,
|
|
8570
|
+
newestCapturedAt: null,
|
|
8571
|
+
nextRetryAt: null,
|
|
8572
|
+
latestError: null
|
|
8573
|
+
},
|
|
8574
|
+
warnings: [],
|
|
8575
|
+
hint: null,
|
|
8576
|
+
metadataWarnings: [],
|
|
8577
|
+
baseline: {
|
|
8578
|
+
lastSnapshotId: null,
|
|
8579
|
+
lastSnapshotHash: null,
|
|
8580
|
+
lastServerHeadHash: null,
|
|
8581
|
+
lastSeenLocalCommitHash: null
|
|
8582
|
+
}
|
|
8583
|
+
};
|
|
8584
|
+
}
|
|
8585
|
+
async function collabDetectRepoState(params) {
|
|
8586
|
+
const detected = buildBaseState();
|
|
8363
8587
|
let repoRoot;
|
|
8364
8588
|
try {
|
|
8365
8589
|
repoRoot = await findGitRoot(params.cwd);
|
|
8366
8590
|
} catch (error) {
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
repoRoot: null,
|
|
8371
|
-
appId: null,
|
|
8372
|
-
currentBranch: null,
|
|
8373
|
-
branchName: null,
|
|
8374
|
-
headCommitHash: null,
|
|
8375
|
-
worktreeClean: false,
|
|
8376
|
-
syncStatus: null,
|
|
8377
|
-
syncTargetCommitHash: null,
|
|
8378
|
-
syncTargetCommitId: null,
|
|
8379
|
-
reconcileTargetHeadCommitHash: null,
|
|
8380
|
-
reconcileTargetHeadCommitId: null,
|
|
8381
|
-
warnings: [],
|
|
8382
|
-
hint: message
|
|
8383
|
-
};
|
|
8591
|
+
detected.status = "not_git_repo";
|
|
8592
|
+
detected.hint = formatCliErrorDetail(error) ?? "Not inside a git repository.";
|
|
8593
|
+
return detected;
|
|
8384
8594
|
}
|
|
8385
|
-
|
|
8595
|
+
detected.repoRoot = repoRoot;
|
|
8596
|
+
const bindingResolution = await resolveActiveLaneBinding({ repoRoot, api: params.api ?? void 0 });
|
|
8386
8597
|
if (bindingResolution.status === "not_bound") {
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
currentBranch: null,
|
|
8392
|
-
branchName: null,
|
|
8393
|
-
headCommitHash: null,
|
|
8394
|
-
worktreeClean: false,
|
|
8395
|
-
syncStatus: null,
|
|
8396
|
-
syncTargetCommitHash: null,
|
|
8397
|
-
syncTargetCommitId: null,
|
|
8398
|
-
reconcileTargetHeadCommitHash: null,
|
|
8399
|
-
reconcileTargetHeadCommitId: null,
|
|
8400
|
-
warnings: [],
|
|
8401
|
-
hint: "Run `remix collab init` first."
|
|
8402
|
-
};
|
|
8598
|
+
detected.status = "not_bound";
|
|
8599
|
+
detected.repoState = "binding_problem";
|
|
8600
|
+
detected.hint = "Run `remix collab init` first.";
|
|
8601
|
+
return detected;
|
|
8403
8602
|
}
|
|
8404
8603
|
if (bindingResolution.status === "missing_branch_binding") {
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
hint: `Current branch ${bindingResolution.currentBranch ?? "(detached)"} is not yet bound to a Remix lane.`
|
|
8420
|
-
};
|
|
8604
|
+
detected.status = "branch_binding_missing";
|
|
8605
|
+
detected.repoState = "binding_problem";
|
|
8606
|
+
detected.currentBranch = bindingResolution.currentBranch;
|
|
8607
|
+
detected.branchName = bindingResolution.currentBranch;
|
|
8608
|
+
detected.hint = `Current branch ${bindingResolution.currentBranch ?? "(detached)"} is not yet bound to a Remix lane.`;
|
|
8609
|
+
return detected;
|
|
8610
|
+
}
|
|
8611
|
+
if (bindingResolution.status === "ambiguous_family_selection") {
|
|
8612
|
+
detected.status = "family_ambiguous";
|
|
8613
|
+
detected.repoState = "binding_problem";
|
|
8614
|
+
detected.currentBranch = bindingResolution.currentBranch;
|
|
8615
|
+
detected.branchName = bindingResolution.currentBranch;
|
|
8616
|
+
detected.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`.";
|
|
8617
|
+
return detected;
|
|
8421
8618
|
}
|
|
8422
8619
|
if (bindingResolution.status === "binding_conflict") {
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
worktreeClean: false,
|
|
8431
|
-
syncStatus: null,
|
|
8432
|
-
syncTargetCommitHash: null,
|
|
8433
|
-
syncTargetCommitId: null,
|
|
8434
|
-
reconcileTargetHeadCommitHash: null,
|
|
8435
|
-
reconcileTargetHeadCommitId: null,
|
|
8436
|
-
warnings: [],
|
|
8437
|
-
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.`
|
|
8438
|
-
};
|
|
8620
|
+
detected.status = "metadata_conflict";
|
|
8621
|
+
detected.repoState = "metadata_conflict";
|
|
8622
|
+
detected.binding = bindingResolution.binding;
|
|
8623
|
+
detected.currentBranch = bindingResolution.currentBranch;
|
|
8624
|
+
detected.branchName = bindingResolution.binding.branchName;
|
|
8625
|
+
detected.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)"}.`;
|
|
8626
|
+
return detected;
|
|
8439
8627
|
}
|
|
8440
8628
|
const binding = bindingResolution.binding;
|
|
8441
|
-
|
|
8629
|
+
detected.binding = binding;
|
|
8630
|
+
const [currentBranch, localCommitHash, worktreeStatus] = await Promise.all([
|
|
8442
8631
|
getCurrentBranch(repoRoot),
|
|
8443
8632
|
getHeadCommitHash(repoRoot),
|
|
8444
8633
|
getWorktreeStatus(repoRoot)
|
|
8445
8634
|
]);
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
syncTargetCommitId: null,
|
|
8477
|
-
reconcileTargetHeadCommitHash: null,
|
|
8478
|
-
reconcileTargetHeadCommitId: null,
|
|
8479
|
-
warnings: [],
|
|
8480
|
-
hint: buildBranchMismatchHint({
|
|
8481
|
-
currentBranch,
|
|
8482
|
-
branchName
|
|
8635
|
+
detected.currentBranch = currentBranch;
|
|
8636
|
+
detected.branchName = binding.branchName ?? null;
|
|
8637
|
+
detected.localCommitHash = localCommitHash;
|
|
8638
|
+
detected.worktreeClean = worktreeStatus.isClean;
|
|
8639
|
+
if (!localCommitHash) {
|
|
8640
|
+
detected.status = "missing_head";
|
|
8641
|
+
detected.repoState = "binding_problem";
|
|
8642
|
+
detected.hint = "Failed to resolve local HEAD commit.";
|
|
8643
|
+
return detected;
|
|
8644
|
+
}
|
|
8645
|
+
if (!params.allowBranchMismatch && !isBoundBranchMatch(currentBranch, binding.branchName ?? null)) {
|
|
8646
|
+
detected.status = "branch_mismatch";
|
|
8647
|
+
detected.repoState = "binding_problem";
|
|
8648
|
+
detected.hint = buildBranchMismatchHint({ currentBranch, branchName: binding.branchName ?? null });
|
|
8649
|
+
return detected;
|
|
8650
|
+
}
|
|
8651
|
+
if (!params.api) {
|
|
8652
|
+
const [inspection, pendingFinalize] = await Promise.all([
|
|
8653
|
+
inspectLocalSnapshot({
|
|
8654
|
+
repoRoot,
|
|
8655
|
+
repoFingerprint: binding.repoFingerprint,
|
|
8656
|
+
laneId: binding.laneId,
|
|
8657
|
+
branchName: binding.branchName,
|
|
8658
|
+
persistBlobs: false
|
|
8659
|
+
}),
|
|
8660
|
+
summarizePendingFinalizeJobs({
|
|
8661
|
+
repoRoot,
|
|
8662
|
+
repoFingerprint: binding.repoFingerprint,
|
|
8663
|
+
currentAppId: binding.currentAppId,
|
|
8664
|
+
laneId: binding.laneId
|
|
8483
8665
|
})
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
repoFingerprint: binding.repoFingerprint ?? void 0,
|
|
8489
|
-
remoteUrl: binding.remoteUrl ?? void 0,
|
|
8490
|
-
defaultBranch: binding.defaultBranch ?? void 0,
|
|
8491
|
-
dryRun: true
|
|
8492
|
-
});
|
|
8493
|
-
const sync = unwrapResponseObject(syncResp, "sync result");
|
|
8494
|
-
if (sync.status === "conflict_risk") {
|
|
8495
|
-
return {
|
|
8496
|
-
status: "metadata_conflict",
|
|
8497
|
-
repoRoot,
|
|
8498
|
-
appId: binding.currentAppId,
|
|
8499
|
-
currentBranch,
|
|
8500
|
-
branchName,
|
|
8501
|
-
headCommitHash,
|
|
8502
|
-
worktreeClean: worktreeStatus.isClean,
|
|
8503
|
-
syncStatus: sync.status,
|
|
8504
|
-
syncTargetCommitHash: sync.targetCommitHash,
|
|
8505
|
-
syncTargetCommitId: sync.targetCommitId,
|
|
8506
|
-
reconcileTargetHeadCommitHash: null,
|
|
8507
|
-
reconcileTargetHeadCommitId: null,
|
|
8508
|
-
warnings: sync.warnings,
|
|
8509
|
-
hint: sync.warnings.join("\n") || "Run the command from the correct bound repository."
|
|
8510
|
-
};
|
|
8511
|
-
}
|
|
8512
|
-
if (sync.status === "up_to_date" || sync.status === "ready_to_fast_forward") {
|
|
8513
|
-
return {
|
|
8514
|
-
status: sync.status,
|
|
8515
|
-
repoRoot,
|
|
8516
|
-
appId: binding.currentAppId,
|
|
8517
|
-
currentBranch,
|
|
8518
|
-
branchName,
|
|
8519
|
-
headCommitHash,
|
|
8520
|
-
worktreeClean: worktreeStatus.isClean,
|
|
8521
|
-
syncStatus: sync.status,
|
|
8522
|
-
syncTargetCommitHash: sync.targetCommitHash,
|
|
8523
|
-
syncTargetCommitId: sync.targetCommitId,
|
|
8524
|
-
reconcileTargetHeadCommitHash: null,
|
|
8525
|
-
reconcileTargetHeadCommitId: null,
|
|
8526
|
-
warnings: sync.warnings,
|
|
8527
|
-
hint: null
|
|
8528
|
-
};
|
|
8529
|
-
}
|
|
8530
|
-
const reconcileResp = await params.api.preflightAppReconcile(binding.currentAppId, {
|
|
8531
|
-
localHeadCommitHash: headCommitHash,
|
|
8532
|
-
repoFingerprint: binding.repoFingerprint ?? void 0,
|
|
8533
|
-
remoteUrl: binding.remoteUrl ?? void 0,
|
|
8534
|
-
defaultBranch: binding.defaultBranch ?? void 0
|
|
8535
|
-
});
|
|
8536
|
-
const reconcile = unwrapResponseObject(reconcileResp, "reconcile preflight");
|
|
8537
|
-
if (reconcile.status === "metadata_conflict") {
|
|
8538
|
-
return {
|
|
8539
|
-
status: "metadata_conflict",
|
|
8540
|
-
repoRoot,
|
|
8541
|
-
appId: binding.currentAppId,
|
|
8542
|
-
currentBranch,
|
|
8543
|
-
branchName,
|
|
8544
|
-
headCommitHash,
|
|
8545
|
-
worktreeClean: worktreeStatus.isClean,
|
|
8546
|
-
syncStatus: sync.status,
|
|
8547
|
-
syncTargetCommitHash: sync.targetCommitHash,
|
|
8548
|
-
syncTargetCommitId: sync.targetCommitId,
|
|
8549
|
-
reconcileTargetHeadCommitHash: reconcile.targetHeadCommitHash,
|
|
8550
|
-
reconcileTargetHeadCommitId: reconcile.targetHeadCommitId,
|
|
8551
|
-
warnings: reconcile.warnings,
|
|
8552
|
-
hint: reconcile.warnings.join("\n") || "Run the command from the correct bound repository."
|
|
8553
|
-
};
|
|
8554
|
-
}
|
|
8555
|
-
if (reconcile.status === "up_to_date") {
|
|
8556
|
-
return {
|
|
8557
|
-
status: "up_to_date",
|
|
8558
|
-
repoRoot,
|
|
8559
|
-
appId: binding.currentAppId,
|
|
8560
|
-
currentBranch,
|
|
8561
|
-
branchName,
|
|
8562
|
-
headCommitHash,
|
|
8563
|
-
worktreeClean: worktreeStatus.isClean,
|
|
8564
|
-
syncStatus: sync.status,
|
|
8565
|
-
syncTargetCommitHash: sync.targetCommitHash,
|
|
8566
|
-
syncTargetCommitId: sync.targetCommitId,
|
|
8567
|
-
reconcileTargetHeadCommitHash: reconcile.targetHeadCommitHash,
|
|
8568
|
-
reconcileTargetHeadCommitId: reconcile.targetHeadCommitId,
|
|
8569
|
-
warnings: reconcile.warnings,
|
|
8570
|
-
hint: null
|
|
8571
|
-
};
|
|
8666
|
+
]);
|
|
8667
|
+
detected.currentSnapshotHash = inspection.snapshotHash;
|
|
8668
|
+
detected.pendingFinalize = pendingFinalize;
|
|
8669
|
+
return detected;
|
|
8572
8670
|
}
|
|
8573
|
-
return {
|
|
8574
|
-
status: "reconcile_required",
|
|
8575
|
-
repoRoot,
|
|
8576
|
-
appId: binding.currentAppId,
|
|
8577
|
-
currentBranch,
|
|
8578
|
-
branchName,
|
|
8579
|
-
headCommitHash,
|
|
8580
|
-
worktreeClean: worktreeStatus.isClean,
|
|
8581
|
-
syncStatus: sync.status,
|
|
8582
|
-
syncTargetCommitHash: sync.targetCommitHash,
|
|
8583
|
-
syncTargetCommitId: sync.targetCommitId,
|
|
8584
|
-
reconcileTargetHeadCommitHash: reconcile.targetHeadCommitHash,
|
|
8585
|
-
reconcileTargetHeadCommitId: reconcile.targetHeadCommitId,
|
|
8586
|
-
warnings: reconcile.warnings,
|
|
8587
|
-
hint: reconcile.warnings.join("\n") || "Run `remix collab reconcile` first because the local history is no longer fast-forward compatible with the app."
|
|
8588
|
-
};
|
|
8589
|
-
}
|
|
8590
|
-
var DEFAULT_ACQUIRE_TIMEOUT_MS = 15e3;
|
|
8591
|
-
var DEFAULT_STALE_MS = 45e3;
|
|
8592
|
-
var DEFAULT_HEARTBEAT_MS = 5e3;
|
|
8593
|
-
var RETRY_DELAY_MS = 250;
|
|
8594
|
-
var heldLocks = /* @__PURE__ */ new Map();
|
|
8595
|
-
function sleep2(ms) {
|
|
8596
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8597
|
-
}
|
|
8598
|
-
function createOwner(params) {
|
|
8599
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8600
|
-
return {
|
|
8601
|
-
operation: params.operation,
|
|
8602
|
-
repoRoot: params.repoRoot,
|
|
8603
|
-
pid: process.pid,
|
|
8604
|
-
hostname: import_os2.default.hostname(),
|
|
8605
|
-
startedAt: now,
|
|
8606
|
-
heartbeatAt: now,
|
|
8607
|
-
version: process.version,
|
|
8608
|
-
requestId: params.requestId?.trim() || null
|
|
8609
|
-
};
|
|
8610
|
-
}
|
|
8611
|
-
async function writeOwnerMetadata(ownerPath, owner) {
|
|
8612
|
-
await import_promises16.default.writeFile(ownerPath, `${JSON.stringify(owner, null, 2)}
|
|
8613
|
-
`, "utf8");
|
|
8614
|
-
}
|
|
8615
|
-
async function readOwnerMetadata(ownerPath) {
|
|
8616
8671
|
try {
|
|
8617
|
-
const
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8672
|
+
const [headResp, inspection, baseline, pendingFinalize] = await Promise.all([
|
|
8673
|
+
params.api.getAppHead(binding.currentAppId),
|
|
8674
|
+
inspectLocalSnapshot({
|
|
8675
|
+
repoRoot,
|
|
8676
|
+
repoFingerprint: binding.repoFingerprint,
|
|
8677
|
+
laneId: binding.laneId,
|
|
8678
|
+
branchName: binding.branchName,
|
|
8679
|
+
persistBlobs: false
|
|
8680
|
+
}),
|
|
8681
|
+
readLocalBaseline({
|
|
8682
|
+
repoFingerprint: binding.repoFingerprint,
|
|
8683
|
+
laneId: binding.laneId,
|
|
8684
|
+
repoRoot
|
|
8685
|
+
}),
|
|
8686
|
+
summarizePendingFinalizeJobs({
|
|
8687
|
+
repoRoot,
|
|
8688
|
+
repoFingerprint: binding.repoFingerprint,
|
|
8689
|
+
currentAppId: binding.currentAppId,
|
|
8690
|
+
laneId: binding.laneId
|
|
8691
|
+
})
|
|
8692
|
+
]);
|
|
8693
|
+
const appHead = unwrapResponseObject(headResp, "app head");
|
|
8694
|
+
detected.currentServerHeadHash = appHead.headCommitHash;
|
|
8695
|
+
detected.currentServerHeadCommitId = appHead.headCommitId;
|
|
8696
|
+
detected.currentSnapshotHash = inspection.snapshotHash;
|
|
8697
|
+
detected.pendingFinalize = pendingFinalize;
|
|
8698
|
+
detected.baseline = {
|
|
8699
|
+
lastSnapshotId: baseline?.lastSnapshotId ?? null,
|
|
8700
|
+
lastSnapshotHash: baseline?.lastSnapshotHash ?? null,
|
|
8701
|
+
lastServerHeadHash: baseline?.lastServerHeadHash ?? null,
|
|
8702
|
+
lastSeenLocalCommitHash: baseline?.lastSeenLocalCommitHash ?? null
|
|
8632
8703
|
};
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8704
|
+
if (!baseline?.lastSnapshotHash || !baseline.lastServerHeadHash) {
|
|
8705
|
+
if (detected.worktreeClean && localCommitHash && localCommitHash !== appHead.headCommitHash) {
|
|
8706
|
+
try {
|
|
8707
|
+
const bootstrapResp = await params.api.getAppDelta(binding.currentAppId, {
|
|
8708
|
+
baseHeadHash: localCommitHash,
|
|
8709
|
+
targetHeadHash: appHead.headCommitHash,
|
|
8710
|
+
repoFingerprint: binding.repoFingerprint ?? void 0,
|
|
8711
|
+
remoteUrl: binding.remoteUrl ?? void 0,
|
|
8712
|
+
defaultBranch: binding.defaultBranch ?? void 0
|
|
8713
|
+
});
|
|
8714
|
+
const bootstrapDelta = unwrapResponseObject(bootstrapResp, "app delta");
|
|
8715
|
+
detected.metadataWarnings = Array.from(/* @__PURE__ */ new Set([...detected.metadataWarnings, ...bootstrapDelta.warnings]));
|
|
8716
|
+
detected.warnings.push(...bootstrapDelta.warnings);
|
|
8717
|
+
if (bootstrapDelta.status === "conflict_risk") {
|
|
8718
|
+
detected.status = "metadata_conflict";
|
|
8719
|
+
detected.repoState = "metadata_conflict";
|
|
8720
|
+
detected.hint = bootstrapDelta.warnings.join("\n") || "Run the command from the correct bound repository.";
|
|
8721
|
+
return detected;
|
|
8722
|
+
}
|
|
8723
|
+
if (bootstrapDelta.status === "delta_ready" || bootstrapDelta.status === "up_to_date") {
|
|
8724
|
+
detected.repoState = "server_only_changed";
|
|
8725
|
+
detected.hint = "This checkout has not stored a local Remix baseline yet, but its current Git HEAD is already known to Remix. Pull the server delta locally to create the first baseline for this checkout.";
|
|
8726
|
+
return detected;
|
|
8727
|
+
}
|
|
8728
|
+
} catch {
|
|
8729
|
+
}
|
|
8730
|
+
}
|
|
8731
|
+
detected.repoState = "external_local_base_changed";
|
|
8732
|
+
detected.hint = "No local Remix baseline exists for this lane yet. Run `remix collab re-anchor` to anchor this checkout.";
|
|
8733
|
+
return detected;
|
|
8734
|
+
}
|
|
8735
|
+
const localHeadMovedSinceBaseline = Boolean(baseline.lastSeenLocalCommitHash) && localCommitHash !== baseline.lastSeenLocalCommitHash;
|
|
8736
|
+
if (localHeadMovedSinceBaseline) {
|
|
8737
|
+
detected.warnings.push(
|
|
8738
|
+
"Local Git HEAD changed since the last Remix baseline. Remix will use the current workspace snapshot to detect divergence."
|
|
8739
|
+
);
|
|
8740
|
+
}
|
|
8741
|
+
const metadataBaseHeadHash = baseline.lastServerHeadHash || appHead.headCommitHash;
|
|
8742
|
+
const metadataResp = await params.api.getAppDelta(binding.currentAppId, {
|
|
8743
|
+
baseHeadHash: metadataBaseHeadHash,
|
|
8744
|
+
targetHeadHash: metadataBaseHeadHash,
|
|
8745
|
+
repoFingerprint: binding.repoFingerprint ?? void 0,
|
|
8746
|
+
remoteUrl: binding.remoteUrl ?? void 0,
|
|
8747
|
+
defaultBranch: binding.defaultBranch ?? void 0
|
|
8748
|
+
});
|
|
8749
|
+
const metadataCheck = unwrapResponseObject(metadataResp, "app delta metadata");
|
|
8750
|
+
detected.metadataWarnings = metadataCheck.warnings;
|
|
8751
|
+
detected.warnings.push(...metadataCheck.warnings);
|
|
8752
|
+
if (metadataCheck.status === "conflict_risk") {
|
|
8753
|
+
detected.status = "metadata_conflict";
|
|
8754
|
+
detected.repoState = "metadata_conflict";
|
|
8755
|
+
detected.hint = metadataCheck.warnings.join("\n") || "Run the command from the correct bound repository.";
|
|
8756
|
+
return detected;
|
|
8757
|
+
}
|
|
8758
|
+
const localChanged = inspection.snapshotHash !== baseline.lastSnapshotHash;
|
|
8759
|
+
const serverChanged = appHead.headCommitHash !== baseline.lastServerHeadHash;
|
|
8760
|
+
if (!localChanged && !serverChanged) {
|
|
8761
|
+
detected.repoState = "idle";
|
|
8762
|
+
return detected;
|
|
8763
|
+
}
|
|
8764
|
+
if (localChanged && !serverChanged) {
|
|
8765
|
+
detected.repoState = "local_only_changed";
|
|
8766
|
+
return detected;
|
|
8767
|
+
}
|
|
8768
|
+
if (!localChanged && serverChanged) {
|
|
8769
|
+
detected.repoState = "server_only_changed";
|
|
8770
|
+
detected.hint = "The server lane advanced since the last agreed baseline. Pull the server delta locally before continuing.";
|
|
8771
|
+
return detected;
|
|
8772
|
+
}
|
|
8773
|
+
detected.repoState = "both_changed";
|
|
8774
|
+
detected.hint = "Both the local workspace and the server lane changed since the last agreed baseline. Replay or reconcile is required before normal recording continues.";
|
|
8775
|
+
return detected;
|
|
8643
8776
|
} catch (error) {
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
return
|
|
8777
|
+
detected.status = "remote_error";
|
|
8778
|
+
detected.hint = formatCliErrorDetail(error) ?? "Failed to detect the current Remix repo state.";
|
|
8779
|
+
return detected;
|
|
8647
8780
|
}
|
|
8648
8781
|
}
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
const
|
|
8653
|
-
|
|
8654
|
-
const stat = await import_promises16.default.stat(ownerPath).catch(() => null);
|
|
8655
|
-
if (stat) return stat.mtimeMs;
|
|
8656
|
-
const dirStat = await import_promises16.default.stat(lockDir).catch(() => null);
|
|
8657
|
-
if (dirStat) return dirStat.mtimeMs;
|
|
8658
|
-
return 0;
|
|
8782
|
+
var FINALIZE_RETRY_BASE_DELAY_MS = 15e3;
|
|
8783
|
+
var FINALIZE_RETRY_MAX_DELAY_MS = 5 * 60 * 1e3;
|
|
8784
|
+
function readMetadataString(job, key) {
|
|
8785
|
+
const value = job.metadata[key];
|
|
8786
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
8659
8787
|
}
|
|
8660
|
-
|
|
8661
|
-
|
|
8788
|
+
function readMetadataActor(job) {
|
|
8789
|
+
const actor = job.metadata.actor;
|
|
8790
|
+
return actor && typeof actor === "object" ? actor : void 0;
|
|
8662
8791
|
}
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
try {
|
|
8668
|
-
await writeOwnerMetadata(ownerPath, owner);
|
|
8669
|
-
} catch (error) {
|
|
8670
|
-
await import_promises16.default.rm(lockDir, { recursive: true, force: true }).catch(() => void 0);
|
|
8671
|
-
throw error;
|
|
8672
|
-
}
|
|
8673
|
-
return true;
|
|
8674
|
-
} catch (error) {
|
|
8675
|
-
if (error?.code === "EEXIST") return false;
|
|
8676
|
-
throw error;
|
|
8677
|
-
}
|
|
8792
|
+
function buildNextRetryAt(retryCount) {
|
|
8793
|
+
const exponent = Math.max(0, retryCount - 1);
|
|
8794
|
+
const delayMs = Math.min(FINALIZE_RETRY_BASE_DELAY_MS * 2 ** exponent, FINALIZE_RETRY_MAX_DELAY_MS);
|
|
8795
|
+
return new Date(Date.now() + delayMs).toISOString();
|
|
8678
8796
|
}
|
|
8679
|
-
function
|
|
8680
|
-
const
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
`Waited ${params.waitedMs}ms for the repo mutation lock.`,
|
|
8688
|
-
`Stale lock threshold: ${params.staleMs}ms.`,
|
|
8689
|
-
"Retry after the active operation finishes. If the process crashed, wait for stale lock recovery or remove the stale lock manually if necessary."
|
|
8690
|
-
];
|
|
8691
|
-
return lines.filter(Boolean).join("\n");
|
|
8797
|
+
function buildFinalizeCliError(params) {
|
|
8798
|
+
const error = new RemixError(params.message, {
|
|
8799
|
+
exitCode: params.exitCode,
|
|
8800
|
+
hint: params.hint
|
|
8801
|
+
});
|
|
8802
|
+
error.finalizeDisposition = params.disposition;
|
|
8803
|
+
error.finalizeReason = params.reason;
|
|
8804
|
+
return error;
|
|
8692
8805
|
}
|
|
8693
|
-
function
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8806
|
+
function classifyFinalizeError(error) {
|
|
8807
|
+
const tagged = error;
|
|
8808
|
+
return {
|
|
8809
|
+
disposition: tagged.finalizeDisposition ?? "retryable",
|
|
8810
|
+
reason: tagged.finalizeReason ?? "unknown",
|
|
8811
|
+
message: error instanceof Error ? error.message : String(error)
|
|
8812
|
+
};
|
|
8698
8813
|
}
|
|
8699
|
-
function
|
|
8814
|
+
function buildWorkspaceMetadata(params) {
|
|
8700
8815
|
return {
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8816
|
+
branch: params.branchName,
|
|
8817
|
+
repoRoot: params.repoRoot,
|
|
8818
|
+
remoteUrl: params.remoteUrl,
|
|
8819
|
+
defaultBranch: params.defaultBranch,
|
|
8820
|
+
recordingMode: "boundary_delta",
|
|
8821
|
+
baselineSnapshotId: params.baselineSnapshotId,
|
|
8822
|
+
currentSnapshotId: params.currentSnapshotId,
|
|
8823
|
+
baselineServerHeadHash: params.baselineServerHeadHash,
|
|
8824
|
+
currentSnapshotHash: params.currentSnapshotHash,
|
|
8825
|
+
localCommitHash: params.localCommitHash,
|
|
8826
|
+
repoStateAtCapture: params.repoState,
|
|
8827
|
+
replayedFromBaseHash: params.replayedFromBaseHash ?? null
|
|
8704
8828
|
};
|
|
8705
8829
|
}
|
|
8706
|
-
async function
|
|
8707
|
-
const
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8830
|
+
async function processClaimedPendingFinalizeJob(params) {
|
|
8831
|
+
const job = params.job;
|
|
8832
|
+
try {
|
|
8833
|
+
const [snapshot, baseline, appHeadResp] = await Promise.all([
|
|
8834
|
+
readLocalSnapshot(job.currentSnapshotId),
|
|
8835
|
+
readLocalBaseline({
|
|
8836
|
+
repoFingerprint: job.repoFingerprint,
|
|
8837
|
+
laneId: job.laneId,
|
|
8838
|
+
repoRoot: job.repoRoot
|
|
8839
|
+
}),
|
|
8840
|
+
params.api.getAppHead(job.currentAppId)
|
|
8841
|
+
]);
|
|
8842
|
+
if (!snapshot) {
|
|
8843
|
+
throw buildFinalizeCliError({
|
|
8844
|
+
message: "Captured snapshot is missing from the local snapshot store.",
|
|
8845
|
+
exitCode: 1,
|
|
8846
|
+
disposition: "terminal",
|
|
8847
|
+
reason: "snapshot_missing"
|
|
8848
|
+
});
|
|
8721
8849
|
}
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8850
|
+
if (!baseline) {
|
|
8851
|
+
throw buildFinalizeCliError({
|
|
8852
|
+
message: "Local baseline is missing for this queued finalize job.",
|
|
8853
|
+
exitCode: 2,
|
|
8854
|
+
hint: "Run `remix collab re-anchor` to anchor the repository again.",
|
|
8855
|
+
disposition: "terminal",
|
|
8856
|
+
reason: "baseline_missing"
|
|
8857
|
+
});
|
|
8858
|
+
}
|
|
8859
|
+
if (baseline.lastSnapshotId !== job.baselineSnapshotId || baseline.lastServerHeadHash !== job.baselineServerHeadHash) {
|
|
8860
|
+
throw buildFinalizeCliError({
|
|
8861
|
+
message: "Finalize queue baseline drifted before this job was processed.",
|
|
8862
|
+
exitCode: 1,
|
|
8863
|
+
hint: "Process queued finalize jobs in capture order, or re-anchor the repository before retrying.",
|
|
8864
|
+
disposition: "terminal",
|
|
8865
|
+
reason: "baseline_drifted"
|
|
8866
|
+
});
|
|
8867
|
+
}
|
|
8868
|
+
const appHead = unwrapResponseObject(appHeadResp, "app head");
|
|
8869
|
+
const remoteUrl = readMetadataString(job, "remoteUrl");
|
|
8870
|
+
const defaultBranch = readMetadataString(job, "defaultBranch");
|
|
8871
|
+
const repoState = readMetadataString(job, "repoState");
|
|
8872
|
+
const actor = readMetadataActor(job);
|
|
8873
|
+
const diffResult = await diffLocalSnapshots({
|
|
8874
|
+
baseSnapshotId: job.baselineSnapshotId,
|
|
8875
|
+
targetSnapshotId: job.currentSnapshotId
|
|
8876
|
+
});
|
|
8877
|
+
if (!diffResult.diff.trim()) {
|
|
8878
|
+
if (appHead.headCommitHash !== job.baselineServerHeadHash) {
|
|
8879
|
+
throw buildFinalizeCliError({
|
|
8880
|
+
message: "Server lane changed before a no-diff turn could be recorded.",
|
|
8881
|
+
exitCode: 2,
|
|
8882
|
+
hint: "Pull the server changes locally before recording another no-diff turn.",
|
|
8883
|
+
disposition: "terminal",
|
|
8884
|
+
reason: "server_lane_changed"
|
|
8885
|
+
});
|
|
8886
|
+
}
|
|
8887
|
+
const collabTurnResp = await params.api.createCollabTurn(job.currentAppId, {
|
|
8888
|
+
threadId: job.threadId ?? void 0,
|
|
8889
|
+
collabLaneId: job.laneId ?? void 0,
|
|
8890
|
+
prompt: job.prompt,
|
|
8891
|
+
assistantResponse: job.assistantResponse,
|
|
8892
|
+
actor,
|
|
8893
|
+
workspaceMetadata: buildWorkspaceMetadata({
|
|
8894
|
+
repoRoot: job.repoRoot,
|
|
8895
|
+
branchName: job.branchName,
|
|
8896
|
+
remoteUrl,
|
|
8897
|
+
defaultBranch,
|
|
8898
|
+
baselineSnapshotId: job.baselineSnapshotId,
|
|
8899
|
+
currentSnapshotId: job.currentSnapshotId,
|
|
8900
|
+
baselineServerHeadHash: job.baselineServerHeadHash,
|
|
8901
|
+
currentSnapshotHash: snapshot.snapshotHash,
|
|
8902
|
+
localCommitHash: snapshot.localCommitHash,
|
|
8903
|
+
repoState
|
|
8904
|
+
}),
|
|
8905
|
+
idempotencyKey: job.idempotencyKey ?? void 0
|
|
8906
|
+
});
|
|
8907
|
+
const collabTurn = unwrapResponseObject(collabTurnResp, "collab turn");
|
|
8908
|
+
await writeLocalBaseline({
|
|
8909
|
+
repoRoot: job.repoRoot,
|
|
8910
|
+
repoFingerprint: job.repoFingerprint,
|
|
8911
|
+
laneId: job.laneId,
|
|
8912
|
+
currentAppId: job.currentAppId,
|
|
8913
|
+
branchName: job.branchName,
|
|
8914
|
+
lastSnapshotId: snapshot.id,
|
|
8915
|
+
lastSnapshotHash: snapshot.snapshotHash,
|
|
8916
|
+
lastServerHeadHash: appHead.headCommitHash,
|
|
8917
|
+
lastSeenLocalCommitHash: snapshot.localCommitHash
|
|
8918
|
+
});
|
|
8919
|
+
await updatePendingFinalizeJob(job.id, {
|
|
8920
|
+
status: "completed",
|
|
8921
|
+
metadata: { collabTurnId: collabTurn.id }
|
|
8922
|
+
});
|
|
8923
|
+
return {
|
|
8924
|
+
mode: "no_diff_turn",
|
|
8925
|
+
idempotencyKey: job.idempotencyKey ?? "",
|
|
8926
|
+
queued: false,
|
|
8927
|
+
jobId: job.id,
|
|
8928
|
+
repoState,
|
|
8929
|
+
changeStep: null,
|
|
8930
|
+
collabTurn,
|
|
8931
|
+
autoSync: null,
|
|
8932
|
+
warnings: []
|
|
8933
|
+
};
|
|
8934
|
+
}
|
|
8935
|
+
let submissionDiff = diffResult.diff;
|
|
8936
|
+
let submissionBaseHeadHash = job.baselineServerHeadHash;
|
|
8937
|
+
let replayedFromBaseHash = null;
|
|
8938
|
+
if (!submissionBaseHeadHash) {
|
|
8939
|
+
throw buildFinalizeCliError({
|
|
8940
|
+
message: "Baseline server head is missing for this finalize job.",
|
|
8941
|
+
exitCode: 1,
|
|
8942
|
+
disposition: "terminal",
|
|
8943
|
+
reason: "baseline_server_head_missing"
|
|
8944
|
+
});
|
|
8945
|
+
}
|
|
8946
|
+
if (appHead.headCommitHash !== submissionBaseHeadHash) {
|
|
8947
|
+
const replayResp = await params.api.startChangeStepReplay(job.currentAppId, {
|
|
8948
|
+
prompt: job.prompt,
|
|
8949
|
+
assistantResponse: job.assistantResponse,
|
|
8950
|
+
diff: diffResult.diff,
|
|
8951
|
+
baseCommitHash: submissionBaseHeadHash,
|
|
8952
|
+
targetHeadCommitHash: appHead.headCommitHash,
|
|
8953
|
+
expectedPaths: diffResult.changedPaths,
|
|
8954
|
+
actor,
|
|
8955
|
+
workspaceMetadata: buildWorkspaceMetadata({
|
|
8956
|
+
repoRoot: job.repoRoot,
|
|
8957
|
+
branchName: job.branchName,
|
|
8958
|
+
remoteUrl,
|
|
8959
|
+
defaultBranch,
|
|
8960
|
+
baselineSnapshotId: job.baselineSnapshotId,
|
|
8961
|
+
currentSnapshotId: job.currentSnapshotId,
|
|
8962
|
+
baselineServerHeadHash: job.baselineServerHeadHash,
|
|
8963
|
+
currentSnapshotHash: snapshot.snapshotHash,
|
|
8964
|
+
localCommitHash: snapshot.localCommitHash,
|
|
8965
|
+
repoState
|
|
8966
|
+
}),
|
|
8967
|
+
idempotencyKey: buildDeterministicIdempotencyKey({
|
|
8968
|
+
kind: "collab_finalize_turn_replay_v1",
|
|
8969
|
+
appId: job.currentAppId,
|
|
8970
|
+
baseCommitHash: submissionBaseHeadHash,
|
|
8971
|
+
targetHeadCommitHash: appHead.headCommitHash,
|
|
8972
|
+
currentSnapshotId: job.currentSnapshotId,
|
|
8973
|
+
diffSha256: diffResult.diffSha256
|
|
8974
|
+
})
|
|
8975
|
+
});
|
|
8976
|
+
const replayStart = unwrapResponseObject(replayResp, "change step replay");
|
|
8977
|
+
const replay = await pollChangeStepReplay(params.api, job.currentAppId, String(replayStart.id));
|
|
8978
|
+
const replayDiffResp = await params.api.getChangeStepReplayDiff(job.currentAppId, replay.id);
|
|
8979
|
+
const replayDiff = unwrapResponseObject(replayDiffResp, "change step replay diff");
|
|
8980
|
+
submissionDiff = replayDiff.diff;
|
|
8981
|
+
replayedFromBaseHash = submissionBaseHeadHash;
|
|
8982
|
+
submissionBaseHeadHash = appHead.headCommitHash;
|
|
8983
|
+
}
|
|
8984
|
+
const changeStepResp = await params.api.createChangeStep(job.currentAppId, {
|
|
8985
|
+
threadId: job.threadId ?? void 0,
|
|
8986
|
+
collabLaneId: job.laneId ?? void 0,
|
|
8987
|
+
prompt: job.prompt,
|
|
8988
|
+
assistantResponse: job.assistantResponse,
|
|
8989
|
+
diff: submissionDiff,
|
|
8990
|
+
baseCommitHash: submissionBaseHeadHash,
|
|
8991
|
+
headCommitHash: submissionBaseHeadHash,
|
|
8992
|
+
changedFilesCount: diffResult.stats.changedFilesCount,
|
|
8993
|
+
insertions: diffResult.stats.insertions,
|
|
8994
|
+
deletions: diffResult.stats.deletions,
|
|
8995
|
+
actor,
|
|
8996
|
+
workspaceMetadata: buildWorkspaceMetadata({
|
|
8997
|
+
repoRoot: job.repoRoot,
|
|
8998
|
+
branchName: job.branchName,
|
|
8999
|
+
remoteUrl,
|
|
9000
|
+
defaultBranch,
|
|
9001
|
+
baselineSnapshotId: job.baselineSnapshotId,
|
|
9002
|
+
currentSnapshotId: job.currentSnapshotId,
|
|
9003
|
+
baselineServerHeadHash: job.baselineServerHeadHash,
|
|
9004
|
+
currentSnapshotHash: snapshot.snapshotHash,
|
|
9005
|
+
localCommitHash: snapshot.localCommitHash,
|
|
9006
|
+
repoState,
|
|
9007
|
+
replayedFromBaseHash
|
|
9008
|
+
}),
|
|
9009
|
+
idempotencyKey: job.idempotencyKey ?? void 0
|
|
9010
|
+
});
|
|
9011
|
+
const createdStep = unwrapResponseObject(changeStepResp, "change step");
|
|
9012
|
+
const changeStep = await pollChangeStep(params.api, job.currentAppId, String(createdStep.id));
|
|
9013
|
+
const nextHeadResp = await params.api.getAppHead(job.currentAppId);
|
|
9014
|
+
const nextHead = unwrapResponseObject(nextHeadResp, "app head");
|
|
9015
|
+
await writeLocalBaseline({
|
|
9016
|
+
repoRoot: job.repoRoot,
|
|
9017
|
+
repoFingerprint: job.repoFingerprint,
|
|
9018
|
+
laneId: job.laneId,
|
|
9019
|
+
currentAppId: job.currentAppId,
|
|
9020
|
+
branchName: job.branchName,
|
|
9021
|
+
lastSnapshotId: snapshot.id,
|
|
9022
|
+
lastSnapshotHash: snapshot.snapshotHash,
|
|
9023
|
+
lastServerHeadHash: nextHead.headCommitHash,
|
|
9024
|
+
lastSeenLocalCommitHash: snapshot.localCommitHash
|
|
9025
|
+
});
|
|
9026
|
+
await updatePendingFinalizeJob(job.id, {
|
|
9027
|
+
status: "completed",
|
|
9028
|
+
metadata: { changeStepId: String(changeStep.id ?? "") }
|
|
8782
9029
|
});
|
|
8783
|
-
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
warnings:
|
|
9030
|
+
return {
|
|
9031
|
+
mode: "changed_turn",
|
|
9032
|
+
idempotencyKey: job.idempotencyKey ?? "",
|
|
9033
|
+
queued: false,
|
|
9034
|
+
jobId: job.id,
|
|
9035
|
+
repoState,
|
|
9036
|
+
changeStep,
|
|
9037
|
+
collabTurn: null,
|
|
9038
|
+
autoSync: null,
|
|
9039
|
+
warnings: []
|
|
9040
|
+
};
|
|
9041
|
+
} catch (error) {
|
|
9042
|
+
const classified = classifyFinalizeError(error);
|
|
9043
|
+
await updatePendingFinalizeJob(job.id, {
|
|
9044
|
+
status: classified.disposition === "terminal" ? "failed" : "queued",
|
|
9045
|
+
error: classified.message,
|
|
9046
|
+
nextRetryAt: classified.disposition === "terminal" ? null : buildNextRetryAt(job.retryCount),
|
|
9047
|
+
metadata: {
|
|
9048
|
+
failureDisposition: classified.disposition,
|
|
9049
|
+
failureReason: classified.reason
|
|
9050
|
+
}
|
|
8793
9051
|
});
|
|
9052
|
+
throw error;
|
|
8794
9053
|
} finally {
|
|
8795
|
-
await
|
|
9054
|
+
await params.release();
|
|
8796
9055
|
}
|
|
8797
9056
|
}
|
|
8798
|
-
async function
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
repoRoot,
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
});
|
|
8819
|
-
const headCommitHash = await getHeadCommitHash(repoRoot);
|
|
8820
|
-
const repoSnapshot = await captureRepoSnapshot(repoRoot);
|
|
8821
|
-
if (!headCommitHash) {
|
|
8822
|
-
throw new RemixError("Failed to resolve local HEAD commit.", { exitCode: 1 });
|
|
8823
|
-
}
|
|
8824
|
-
const resp = await params.api.syncLocalApp(binding.currentAppId, {
|
|
8825
|
-
baseCommitHash: headCommitHash,
|
|
8826
|
-
repoFingerprint: binding.repoFingerprint ?? void 0,
|
|
8827
|
-
remoteUrl: binding.remoteUrl ?? void 0,
|
|
8828
|
-
defaultBranch: binding.defaultBranch ?? void 0,
|
|
8829
|
-
dryRun: params.dryRun
|
|
9057
|
+
async function enqueueCapturedFinalizeTurn(params) {
|
|
9058
|
+
return enqueuePendingFinalizeJob({
|
|
9059
|
+
status: "queued",
|
|
9060
|
+
repoRoot: params.repoRoot,
|
|
9061
|
+
repoFingerprint: params.repoFingerprint,
|
|
9062
|
+
currentAppId: params.currentAppId,
|
|
9063
|
+
laneId: params.laneId,
|
|
9064
|
+
threadId: params.threadId,
|
|
9065
|
+
branchName: params.branchName,
|
|
9066
|
+
prompt: params.prompt,
|
|
9067
|
+
assistantResponse: params.assistantResponse,
|
|
9068
|
+
baselineSnapshotId: params.baselineSnapshotId,
|
|
9069
|
+
baselineServerHeadHash: params.baselineServerHeadHash,
|
|
9070
|
+
currentSnapshotId: params.currentSnapshotId,
|
|
9071
|
+
idempotencyKey: params.idempotencyKey,
|
|
9072
|
+
error: null,
|
|
9073
|
+
retryCount: 0,
|
|
9074
|
+
lastAttemptAt: null,
|
|
9075
|
+
nextRetryAt: null,
|
|
9076
|
+
metadata: params.metadata ?? {}
|
|
8830
9077
|
});
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
if (sync.status === "up_to_date") {
|
|
8845
|
-
return {
|
|
8846
|
-
status: sync.status,
|
|
8847
|
-
branch,
|
|
8848
|
-
repoRoot,
|
|
8849
|
-
baseCommitHash: sync.baseCommitHash,
|
|
8850
|
-
targetCommitHash: sync.targetCommitHash,
|
|
8851
|
-
targetCommitId: sync.targetCommitId,
|
|
8852
|
-
stats: sync.stats,
|
|
8853
|
-
localCommitHash: headCommitHash,
|
|
8854
|
-
applied: false,
|
|
8855
|
-
dryRun: params.dryRun
|
|
8856
|
-
};
|
|
8857
|
-
}
|
|
8858
|
-
const previewResult = {
|
|
8859
|
-
status: sync.status,
|
|
8860
|
-
branch,
|
|
8861
|
-
repoRoot,
|
|
8862
|
-
baseCommitHash: sync.baseCommitHash,
|
|
8863
|
-
targetCommitHash: sync.targetCommitHash,
|
|
8864
|
-
targetCommitId: sync.targetCommitId,
|
|
8865
|
-
stats: sync.stats,
|
|
8866
|
-
bundleRef: sync.bundleRef,
|
|
8867
|
-
bundleSizeBytes: sync.bundleSizeBytes,
|
|
8868
|
-
localCommitHash: headCommitHash,
|
|
8869
|
-
applied: false,
|
|
8870
|
-
dryRun: params.dryRun
|
|
8871
|
-
};
|
|
8872
|
-
if (params.dryRun) {
|
|
8873
|
-
return previewResult;
|
|
8874
|
-
}
|
|
8875
|
-
if (!sync.bundleBase64 || !sync.bundleRef) {
|
|
8876
|
-
throw new RemixError("Sync bundle payload is missing.", { exitCode: 1 });
|
|
8877
|
-
}
|
|
8878
|
-
const bundleBase64 = sync.bundleBase64;
|
|
8879
|
-
const bundleRef = sync.bundleRef;
|
|
8880
|
-
return withRepoMutationLock(
|
|
8881
|
-
{
|
|
8882
|
-
cwd: repoRoot,
|
|
8883
|
-
operation: "collabSync"
|
|
8884
|
-
},
|
|
8885
|
-
async ({ repoRoot: lockedRepoRoot, warnings }) => {
|
|
8886
|
-
await assertRepoSnapshotUnchanged(lockedRepoRoot, repoSnapshot, {
|
|
8887
|
-
operation: "`remix collab sync`",
|
|
8888
|
-
recoveryHint: "The repository changed after sync was prepared. Review the local changes and rerun `remix collab sync`."
|
|
8889
|
-
});
|
|
8890
|
-
await ensureCleanWorktree(lockedRepoRoot);
|
|
8891
|
-
const lockedBranch = await requireCurrentBranch(lockedRepoRoot);
|
|
8892
|
-
assertBoundBranchMatch({
|
|
8893
|
-
currentBranch: lockedBranch,
|
|
8894
|
-
branchName: binding.branchName,
|
|
8895
|
-
allowBranchMismatch: params.allowBranchMismatch,
|
|
8896
|
-
operation: "`remix collab sync`"
|
|
9078
|
+
}
|
|
9079
|
+
async function drainPendingFinalizeQueue(params) {
|
|
9080
|
+
await prunePendingFinalizeJobs();
|
|
9081
|
+
const jobs = await listPendingFinalizeJobs();
|
|
9082
|
+
const results = [];
|
|
9083
|
+
for (const job of jobs) {
|
|
9084
|
+
const claimed = await claimPendingFinalizeJob(job.id);
|
|
9085
|
+
if (!claimed) continue;
|
|
9086
|
+
try {
|
|
9087
|
+
const result = await processClaimedPendingFinalizeJob({
|
|
9088
|
+
api: params.api,
|
|
9089
|
+
job: claimed.job,
|
|
9090
|
+
release: claimed.release
|
|
8897
9091
|
});
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
await import_promises17.default.writeFile(bundlePath, Buffer.from(bundleBase64, "base64"));
|
|
8902
|
-
await importGitBundle(lockedRepoRoot, bundlePath, bundleRef);
|
|
8903
|
-
await ensureCommitExists(lockedRepoRoot, sync.targetCommitHash);
|
|
8904
|
-
const localCommitHash = await fastForwardToCommit(lockedRepoRoot, sync.targetCommitHash);
|
|
8905
|
-
return {
|
|
8906
|
-
...previewResult,
|
|
8907
|
-
localCommitHash,
|
|
8908
|
-
applied: true,
|
|
8909
|
-
dryRun: false,
|
|
8910
|
-
...warnings.length > 0 ? { warnings } : {}
|
|
8911
|
-
};
|
|
8912
|
-
} finally {
|
|
8913
|
-
await import_promises17.default.rm(tempDir, { recursive: true, force: true });
|
|
8914
|
-
}
|
|
9092
|
+
results.push(result);
|
|
9093
|
+
await removePendingFinalizeJob(job.id);
|
|
9094
|
+
} catch {
|
|
8915
9095
|
}
|
|
8916
|
-
);
|
|
8917
|
-
}
|
|
8918
|
-
function assertSupportedRecordingPreflight(preflight) {
|
|
8919
|
-
if (preflight.status === "not_bound") {
|
|
8920
|
-
throw new RemixError("Repository is not bound to Remix.", {
|
|
8921
|
-
exitCode: 2,
|
|
8922
|
-
hint: preflight.hint
|
|
8923
|
-
});
|
|
8924
|
-
}
|
|
8925
|
-
if (preflight.status === "branch_binding_missing") {
|
|
8926
|
-
throw new RemixError("Current branch is not yet bound to a Remix lane.", {
|
|
8927
|
-
exitCode: 2,
|
|
8928
|
-
hint: preflight.hint
|
|
8929
|
-
});
|
|
8930
|
-
}
|
|
8931
|
-
if (preflight.status === "not_git_repo") {
|
|
8932
|
-
throw new RemixError(preflight.hint || "Not inside a git repository.", {
|
|
8933
|
-
exitCode: 2,
|
|
8934
|
-
hint: preflight.hint
|
|
8935
|
-
});
|
|
8936
|
-
}
|
|
8937
|
-
if (preflight.status === "missing_head") {
|
|
8938
|
-
throw new RemixError("Failed to resolve local HEAD commit.", {
|
|
8939
|
-
exitCode: 1,
|
|
8940
|
-
hint: preflight.hint
|
|
8941
|
-
});
|
|
8942
|
-
}
|
|
8943
|
-
if (preflight.status === "branch_mismatch") {
|
|
8944
|
-
assertBoundBranchMatch({
|
|
8945
|
-
currentBranch: preflight.currentBranch,
|
|
8946
|
-
branchName: preflight.branchName,
|
|
8947
|
-
allowBranchMismatch: false,
|
|
8948
|
-
operation: "`remix collab add`"
|
|
8949
|
-
});
|
|
8950
|
-
}
|
|
8951
|
-
if (preflight.status === "metadata_conflict") {
|
|
8952
|
-
throw new RemixError("Local repository metadata conflicts with the bound Remix app.", {
|
|
8953
|
-
exitCode: 2,
|
|
8954
|
-
hint: preflight.hint
|
|
8955
|
-
});
|
|
8956
|
-
}
|
|
8957
|
-
if (preflight.status === "reconcile_required") {
|
|
8958
|
-
throw new RemixError("Local repository cannot be fast-forward synced.", {
|
|
8959
|
-
exitCode: 2,
|
|
8960
|
-
hint: preflight.hint
|
|
8961
|
-
});
|
|
8962
9096
|
}
|
|
9097
|
+
return results;
|
|
8963
9098
|
}
|
|
8964
|
-
|
|
9099
|
+
function collectWarnings(value) {
|
|
9100
|
+
if (!Array.isArray(value)) return [];
|
|
9101
|
+
return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
9102
|
+
}
|
|
9103
|
+
var FINALIZE_QUEUED_WARNING = "Queued only: the local Remix turn was captured, but no remote change step or collab turn exists yet. Drain or await finalize before merge-related flows.";
|
|
9104
|
+
async function collabFinalizeTurn(params) {
|
|
8965
9105
|
const repoRoot = await findGitRoot(params.cwd);
|
|
8966
9106
|
const binding = await ensureActiveLaneBinding({
|
|
8967
9107
|
repoRoot,
|
|
8968
9108
|
api: params.api,
|
|
8969
|
-
operation: "`remix collab
|
|
9109
|
+
operation: "`remix collab finalize-turn`"
|
|
8970
9110
|
});
|
|
8971
9111
|
if (!binding) {
|
|
8972
9112
|
throw new RemixError("Repository is not bound to Remix.", {
|
|
@@ -8975,379 +9115,114 @@ async function collabAdd(params) {
|
|
|
8975
9115
|
});
|
|
8976
9116
|
}
|
|
8977
9117
|
const prompt = params.prompt.trim();
|
|
9118
|
+
const assistantResponse = params.assistantResponse.trim();
|
|
8978
9119
|
if (!prompt) throw new RemixError("Prompt is required.", { exitCode: 2 });
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
const autoSyncEnabled = params.sync !== false;
|
|
8983
|
-
const run = async (lockWarnings = []) => {
|
|
8984
|
-
const preflight = await collabRecordingPreflight({
|
|
8985
|
-
api: params.api,
|
|
8986
|
-
cwd: repoRoot,
|
|
8987
|
-
allowBranchMismatch: params.allowBranchMismatch
|
|
8988
|
-
});
|
|
8989
|
-
assertSupportedRecordingPreflight(preflight);
|
|
8990
|
-
const branch = preflight.currentBranch;
|
|
8991
|
-
assertBoundBranchMatch({
|
|
8992
|
-
currentBranch: branch,
|
|
8993
|
-
branchName: binding.branchName,
|
|
8994
|
-
allowBranchMismatch: params.allowBranchMismatch,
|
|
8995
|
-
operation: "`remix collab add`"
|
|
8996
|
-
});
|
|
8997
|
-
let headCommitHash = preflight.headCommitHash;
|
|
8998
|
-
if (!headCommitHash) {
|
|
8999
|
-
throw new RemixError("Failed to resolve local HEAD commit.", { exitCode: 1 });
|
|
9000
|
-
}
|
|
9001
|
-
const worktreeStatus = await getWorktreeStatus(repoRoot);
|
|
9002
|
-
if (preflight.status === "ready_to_fast_forward") {
|
|
9003
|
-
if (!autoSyncEnabled) {
|
|
9004
|
-
throw new RemixError("Local repository is stale and `collab add` sync automation is disabled.", {
|
|
9005
|
-
exitCode: 2,
|
|
9006
|
-
hint: "Run `remix collab sync` first, or rerun without disabling sync automation."
|
|
9007
|
-
});
|
|
9008
|
-
}
|
|
9009
|
-
if (!worktreeStatus.isClean && diffSource !== "worktree") {
|
|
9010
|
-
throw new RemixError("Automatic stale-work replay requires the current worktree diff.", {
|
|
9011
|
-
exitCode: 2,
|
|
9012
|
-
hint: "Use `remix collab add` without an external diff while the local repo is dirty, or clean the repo before submitting an external diff."
|
|
9013
|
-
});
|
|
9014
|
-
}
|
|
9015
|
-
if (worktreeStatus.isClean) {
|
|
9016
|
-
await collabSync({
|
|
9017
|
-
api: params.api,
|
|
9018
|
-
cwd: repoRoot,
|
|
9019
|
-
dryRun: false,
|
|
9020
|
-
allowBranchMismatch: params.allowBranchMismatch
|
|
9021
|
-
});
|
|
9022
|
-
headCommitHash = await getHeadCommitHash(repoRoot);
|
|
9023
|
-
if (!headCommitHash) {
|
|
9024
|
-
throw new RemixError("Failed to resolve local HEAD after syncing.", { exitCode: 1 });
|
|
9025
|
-
}
|
|
9026
|
-
} else {
|
|
9027
|
-
const staleWorkSnapshot = await captureRepoSnapshot(repoRoot, { includeWorkspaceDiffHash: true });
|
|
9028
|
-
const preserved = await preserveWorkspaceChanges(repoRoot, "remix-add-preserve");
|
|
9029
|
-
try {
|
|
9030
|
-
await assertRepoSnapshotUnchanged(repoRoot, staleWorkSnapshot, {
|
|
9031
|
-
operation: "`remix collab add` stale-work pre-sync",
|
|
9032
|
-
recoveryHint: "The worktree changed while local changes were being preserved. Review the local changes and rerun `remix collab add`."
|
|
9033
|
-
});
|
|
9034
|
-
await discardTrackedChanges(repoRoot, "`remix collab add`");
|
|
9035
|
-
await discardCapturedUntrackedChanges(repoRoot, preserved.includedUntrackedPaths);
|
|
9036
|
-
await collabSync({
|
|
9037
|
-
api: params.api,
|
|
9038
|
-
cwd: repoRoot,
|
|
9039
|
-
dryRun: false,
|
|
9040
|
-
allowBranchMismatch: params.allowBranchMismatch
|
|
9041
|
-
});
|
|
9042
|
-
} catch (err) {
|
|
9043
|
-
const detail = formatCliErrorDetail(err);
|
|
9044
|
-
const hint = [
|
|
9045
|
-
detail,
|
|
9046
|
-
`The preserved local diff is available at: ${preserved.preservedDiffPath}`
|
|
9047
|
-
].filter(Boolean).join("\n\n");
|
|
9048
|
-
throw new RemixError("Failed to sync the stale repository before submitting the change step.", {
|
|
9049
|
-
exitCode: err instanceof RemixError ? err.exitCode : 1,
|
|
9050
|
-
hint
|
|
9051
|
-
});
|
|
9052
|
-
}
|
|
9053
|
-
headCommitHash = await getHeadCommitHash(repoRoot);
|
|
9054
|
-
if (!headCommitHash) {
|
|
9055
|
-
throw new RemixError("Failed to resolve local HEAD after syncing.", { exitCode: 1 });
|
|
9056
|
-
}
|
|
9057
|
-
const deterministicReapply = await reapplyPreservedWorkspaceChanges(repoRoot, preserved);
|
|
9058
|
-
if (deterministicReapply.status === "failed") {
|
|
9059
|
-
const hint = [
|
|
9060
|
-
deterministicReapply.detail,
|
|
9061
|
-
`The preserved local diff is available at: ${preserved.preservedDiffPath}`
|
|
9062
|
-
].filter(Boolean).join("\n\n");
|
|
9063
|
-
throw new RemixError("Failed to restore preserved local changes after syncing.", {
|
|
9064
|
-
exitCode: 1,
|
|
9065
|
-
hint
|
|
9066
|
-
});
|
|
9067
|
-
}
|
|
9068
|
-
if (deterministicReapply.status === "conflict") {
|
|
9069
|
-
try {
|
|
9070
|
-
const replayResp = await params.api.startChangeStepReplay(binding.currentAppId, {
|
|
9071
|
-
prompt,
|
|
9072
|
-
assistantResponse: assistantResponse ?? void 0,
|
|
9073
|
-
diff: await import_promises15.default.readFile(preserved.preservedDiffPath, "utf8"),
|
|
9074
|
-
baseCommitHash: preserved.baseHeadCommitHash,
|
|
9075
|
-
targetHeadCommitHash: headCommitHash,
|
|
9076
|
-
expectedPaths: preserved.stagePlan.expectedPaths,
|
|
9077
|
-
actor: params.actor,
|
|
9078
|
-
workspaceMetadata: {
|
|
9079
|
-
branch,
|
|
9080
|
-
repoRoot,
|
|
9081
|
-
remoteUrl: binding.remoteUrl,
|
|
9082
|
-
defaultBranch: binding.defaultBranch
|
|
9083
|
-
},
|
|
9084
|
-
idempotencyKey: buildDeterministicIdempotencyKey({
|
|
9085
|
-
appId: binding.currentAppId,
|
|
9086
|
-
baseCommitHash: preserved.baseHeadCommitHash,
|
|
9087
|
-
targetHeadCommitHash: headCommitHash,
|
|
9088
|
-
prompt,
|
|
9089
|
-
assistantResponse,
|
|
9090
|
-
preservedDiffSha256: preserved.preservedDiffSha256
|
|
9091
|
-
})
|
|
9092
|
-
});
|
|
9093
|
-
const startedReplay = unwrapResponseObject(replayResp, "change step replay");
|
|
9094
|
-
const replay = await pollChangeStepReplay(params.api, binding.currentAppId, String(startedReplay.id));
|
|
9095
|
-
const replayDiffResp = await params.api.getChangeStepReplayDiff(binding.currentAppId, String(replay.id));
|
|
9096
|
-
const replayDiff = unwrapResponseObject(replayDiffResp, "change step replay diff");
|
|
9097
|
-
const { backupPath: backupPath2, diffSha256 } = await writeTempUnifiedDiffBackup(replayDiff.diff, "remix-add-ai-replay");
|
|
9098
|
-
const replayApply = await reapplyPreservedWorkspaceChanges(repoRoot, {
|
|
9099
|
-
baseHeadCommitHash: headCommitHash,
|
|
9100
|
-
preservedDiffPath: backupPath2,
|
|
9101
|
-
preservedDiffSha256: diffSha256,
|
|
9102
|
-
includedUntrackedPaths: [],
|
|
9103
|
-
stagePlan: preserved.stagePlan
|
|
9104
|
-
});
|
|
9105
|
-
if (replayApply.status !== "clean") {
|
|
9106
|
-
const hint = [
|
|
9107
|
-
replayApply.detail,
|
|
9108
|
-
`The preserved local diff is available at: ${preserved.preservedDiffPath}`,
|
|
9109
|
-
`The AI-replayed diff is available at: ${backupPath2}`
|
|
9110
|
-
].filter(Boolean).join("\n\n");
|
|
9111
|
-
throw new RemixError("AI-assisted stale-work replay produced a diff that could not be applied locally.", {
|
|
9112
|
-
exitCode: 1,
|
|
9113
|
-
hint
|
|
9114
|
-
});
|
|
9115
|
-
}
|
|
9116
|
-
} catch (err) {
|
|
9117
|
-
const detail = formatCliErrorDetail(err);
|
|
9118
|
-
const hint = [
|
|
9119
|
-
detail,
|
|
9120
|
-
`The preserved local diff is available at: ${preserved.preservedDiffPath}`,
|
|
9121
|
-
"Resolve the local conflict manually if needed, then rerun `remix collab add`."
|
|
9122
|
-
].filter(Boolean).join("\n\n");
|
|
9123
|
-
throw new RemixError("AI-assisted stale-work replay could not complete safely.", {
|
|
9124
|
-
exitCode: err instanceof RemixError ? err.exitCode : 1,
|
|
9125
|
-
hint
|
|
9126
|
-
});
|
|
9127
|
-
}
|
|
9128
|
-
}
|
|
9129
|
-
}
|
|
9130
|
-
}
|
|
9131
|
-
const workspaceSnapshot = diffSource === "external" ? null : await getWorkspaceSnapshot(repoRoot);
|
|
9132
|
-
const submissionSnapshot = diffSource === "worktree" ? await captureRepoSnapshot(repoRoot, { includeWorkspaceDiffHash: true }) : null;
|
|
9133
|
-
const diff = params.diff ?? workspaceSnapshot?.diff ?? "";
|
|
9134
|
-
if (!diff.trim()) {
|
|
9135
|
-
throw new RemixError("Diff is empty.", {
|
|
9136
|
-
exitCode: 2,
|
|
9137
|
-
hint: "Make changes first, or pass `--diff-file`/`--diff-stdin`."
|
|
9138
|
-
});
|
|
9139
|
-
}
|
|
9140
|
-
if (diffSource === "external") {
|
|
9141
|
-
const validation = await validateUnifiedDiff(repoRoot, diff);
|
|
9142
|
-
if (!validation.ok) {
|
|
9143
|
-
const actionHint = validation.kind === "malformed_patch" ? "The provided external diff is malformed. Recreate it with `git diff --binary --no-ext-diff`, avoid hand-editing patch hunks, and ensure the patch ends with a trailing newline." : validation.kind === "apply_conflict" ? "The external diff is valid patch syntax, but it does not apply cleanly to the current local HEAD. Sync or update the repo and regenerate the diff against the latest base." : "Git could not validate the provided external diff against the current repository state.";
|
|
9144
|
-
const hint = [validation.detail, actionHint].filter(Boolean).join("\n\n");
|
|
9145
|
-
throw new RemixError("External diff validation failed.", {
|
|
9146
|
-
exitCode: validation.kind === "malformed_patch" ? 2 : 1,
|
|
9147
|
-
hint
|
|
9148
|
-
});
|
|
9149
|
-
}
|
|
9150
|
-
}
|
|
9151
|
-
headCommitHash = await getHeadCommitHash(repoRoot);
|
|
9152
|
-
if (!headCommitHash) {
|
|
9153
|
-
throw new RemixError("Failed to resolve local HEAD before creating the change step.", { exitCode: 1 });
|
|
9154
|
-
}
|
|
9155
|
-
const stats = summarizeUnifiedDiff(diff);
|
|
9156
|
-
const idempotencyKey = params.idempotencyKey?.trim() || buildDeterministicIdempotencyKey({
|
|
9157
|
-
appId: binding.currentAppId,
|
|
9158
|
-
upstreamAppId: binding.upstreamAppId,
|
|
9159
|
-
headCommitHash,
|
|
9160
|
-
prompt,
|
|
9161
|
-
assistantResponse,
|
|
9162
|
-
diff
|
|
9163
|
-
});
|
|
9164
|
-
const resp = await params.api.createChangeStep(binding.currentAppId, {
|
|
9165
|
-
threadId: binding.threadId ?? void 0,
|
|
9166
|
-
collabLaneId: binding.laneId ?? void 0,
|
|
9167
|
-
prompt,
|
|
9168
|
-
assistantResponse: assistantResponse ?? void 0,
|
|
9169
|
-
diff,
|
|
9170
|
-
baseCommitHash: headCommitHash,
|
|
9171
|
-
headCommitHash,
|
|
9172
|
-
changedFilesCount: stats.changedFilesCount,
|
|
9173
|
-
insertions: stats.insertions,
|
|
9174
|
-
deletions: stats.deletions,
|
|
9175
|
-
actor: params.actor,
|
|
9176
|
-
workspaceMetadata: {
|
|
9177
|
-
branch,
|
|
9178
|
-
repoRoot,
|
|
9179
|
-
remoteUrl: binding.remoteUrl,
|
|
9180
|
-
defaultBranch: binding.defaultBranch
|
|
9181
|
-
},
|
|
9182
|
-
idempotencyKey
|
|
9183
|
-
});
|
|
9184
|
-
const created = unwrapResponseObject(resp, "change step");
|
|
9185
|
-
const step = await pollChangeStep(params.api, binding.currentAppId, String(created.id));
|
|
9186
|
-
const canAutoSyncLocally = autoSyncEnabled && diffSource === "worktree";
|
|
9187
|
-
if (!autoSyncEnabled || !canAutoSyncLocally) {
|
|
9188
|
-
return attachWarnings(step, lockWarnings);
|
|
9189
|
-
}
|
|
9190
|
-
const { backupPath } = await writeTempUnifiedDiffBackup(diff, "remix-add");
|
|
9191
|
-
try {
|
|
9192
|
-
await pollAppReady(params.api, binding.currentAppId);
|
|
9193
|
-
if (submissionSnapshot) {
|
|
9194
|
-
await assertRepoSnapshotUnchanged(repoRoot, submissionSnapshot, {
|
|
9195
|
-
operation: "`remix collab add` auto-sync",
|
|
9196
|
-
recoveryHint: "The repository changed after the change step was submitted. Review the local changes, inspect the preserved diff if needed, and rerun `remix collab sync` manually."
|
|
9197
|
-
});
|
|
9198
|
-
}
|
|
9199
|
-
await discardTrackedChanges(repoRoot, "`remix collab add`");
|
|
9200
|
-
await discardCapturedUntrackedChanges(repoRoot, workspaceSnapshot?.includedUntrackedPaths ?? []);
|
|
9201
|
-
await collabSync({
|
|
9202
|
-
api: params.api,
|
|
9203
|
-
cwd: repoRoot,
|
|
9204
|
-
dryRun: false,
|
|
9205
|
-
allowBranchMismatch: params.allowBranchMismatch
|
|
9206
|
-
});
|
|
9207
|
-
await import_promises15.default.rm(import_path4.default.dirname(backupPath), { recursive: true, force: true }).catch(() => void 0);
|
|
9208
|
-
} catch (err) {
|
|
9209
|
-
const detail = formatCliErrorDetail(err);
|
|
9210
|
-
const hint = [
|
|
9211
|
-
detail,
|
|
9212
|
-
`The submitted diff backup was preserved at: ${backupPath}`,
|
|
9213
|
-
"The change step already succeeded remotely. Inspect or reapply that diff manually if needed, then run `remix collab sync`."
|
|
9214
|
-
].filter(Boolean).join("\n\n");
|
|
9215
|
-
throw new RemixError("Change step succeeded remotely, but automatic local sync failed.", {
|
|
9216
|
-
exitCode: err instanceof RemixError ? err.exitCode : 1,
|
|
9217
|
-
hint
|
|
9218
|
-
});
|
|
9219
|
-
}
|
|
9220
|
-
return attachWarnings(step, lockWarnings);
|
|
9221
|
-
};
|
|
9222
|
-
if (diffSource === "worktree") {
|
|
9223
|
-
return withRepoMutationLock(
|
|
9224
|
-
{
|
|
9225
|
-
cwd: repoRoot,
|
|
9226
|
-
operation: "collabAdd"
|
|
9227
|
-
},
|
|
9228
|
-
async ({ warnings }) => run(warnings)
|
|
9229
|
-
);
|
|
9230
|
-
}
|
|
9231
|
-
return run();
|
|
9232
|
-
}
|
|
9233
|
-
function assertSupportedRecordingPreflight2(preflight) {
|
|
9234
|
-
if (preflight.status === "not_bound") {
|
|
9235
|
-
throw new RemixError("Repository is not bound to Remix.", {
|
|
9120
|
+
if (!assistantResponse) throw new RemixError("Assistant response is required.", { exitCode: 2 });
|
|
9121
|
+
if (params.diff?.trim()) {
|
|
9122
|
+
throw new RemixError("External diff submission is no longer supported for `finalize_turn`.", {
|
|
9236
9123
|
exitCode: 2,
|
|
9237
|
-
hint:
|
|
9124
|
+
hint: "Finalize turns now capture the real workspace boundary from the local snapshot store."
|
|
9238
9125
|
});
|
|
9239
9126
|
}
|
|
9240
|
-
|
|
9241
|
-
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
|
|
9127
|
+
const detected = await collabDetectRepoState({
|
|
9128
|
+
api: params.api,
|
|
9129
|
+
cwd: repoRoot,
|
|
9130
|
+
allowBranchMismatch: params.allowBranchMismatch
|
|
9131
|
+
});
|
|
9132
|
+
if (detected.status === "not_bound") {
|
|
9133
|
+
throw new RemixError("Repository is not bound to Remix.", { exitCode: 2, hint: detected.hint });
|
|
9245
9134
|
}
|
|
9246
|
-
if (
|
|
9247
|
-
throw new RemixError(
|
|
9135
|
+
if (detected.status === "branch_binding_missing" || detected.status === "family_ambiguous") {
|
|
9136
|
+
throw new RemixError(detected.hint || "Current branch is not ready for Remix recording.", { exitCode: 2, hint: detected.hint });
|
|
9137
|
+
}
|
|
9138
|
+
if (detected.status === "metadata_conflict" || detected.status === "branch_mismatch") {
|
|
9139
|
+
throw new RemixError("Repository must be realigned before finalizing the turn.", {
|
|
9248
9140
|
exitCode: 2,
|
|
9249
|
-
hint:
|
|
9141
|
+
hint: detected.hint
|
|
9250
9142
|
});
|
|
9251
9143
|
}
|
|
9252
|
-
if (
|
|
9253
|
-
throw new RemixError("Failed to
|
|
9144
|
+
if (detected.status === "missing_head" || detected.status === "remote_error") {
|
|
9145
|
+
throw new RemixError(detected.hint || "Failed to determine the current repo state.", {
|
|
9254
9146
|
exitCode: 1,
|
|
9255
|
-
hint:
|
|
9256
|
-
});
|
|
9257
|
-
}
|
|
9258
|
-
if (preflight.status === "branch_mismatch") {
|
|
9259
|
-
assertBoundBranchMatch({
|
|
9260
|
-
currentBranch: preflight.currentBranch,
|
|
9261
|
-
branchName: preflight.branchName,
|
|
9262
|
-
allowBranchMismatch: false,
|
|
9263
|
-
operation: "`remix collab record-turn`"
|
|
9147
|
+
hint: detected.hint
|
|
9264
9148
|
});
|
|
9265
9149
|
}
|
|
9266
|
-
if (
|
|
9267
|
-
throw new RemixError("
|
|
9150
|
+
if (detected.repoState === "server_only_changed") {
|
|
9151
|
+
throw new RemixError("Server changes must be pulled locally before finalizing this turn.", {
|
|
9268
9152
|
exitCode: 2,
|
|
9269
|
-
hint:
|
|
9153
|
+
hint: detected.hint
|
|
9270
9154
|
});
|
|
9271
9155
|
}
|
|
9272
|
-
if (
|
|
9273
|
-
throw new RemixError("
|
|
9274
|
-
exitCode: 2,
|
|
9275
|
-
hint: preflight.hint
|
|
9276
|
-
});
|
|
9277
|
-
}
|
|
9278
|
-
}
|
|
9279
|
-
async function collabRecordTurn(params) {
|
|
9280
|
-
const repoRoot = await findGitRoot(params.cwd);
|
|
9281
|
-
const binding = await ensureActiveLaneBinding({
|
|
9282
|
-
repoRoot,
|
|
9283
|
-
api: params.api,
|
|
9284
|
-
operation: "`remix collab record-turn`"
|
|
9285
|
-
});
|
|
9286
|
-
if (!binding) {
|
|
9287
|
-
throw new RemixError("Repository is not bound to Remix.", {
|
|
9156
|
+
if (detected.repoState === "external_local_base_changed") {
|
|
9157
|
+
throw new RemixError("The local checkout must be re-anchored before finalizing this turn.", {
|
|
9288
9158
|
exitCode: 2,
|
|
9289
|
-
hint:
|
|
9159
|
+
hint: detected.hint
|
|
9290
9160
|
});
|
|
9291
9161
|
}
|
|
9292
|
-
const
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
const preflight = await collabRecordingPreflight({
|
|
9297
|
-
api: params.api,
|
|
9298
|
-
cwd: repoRoot,
|
|
9299
|
-
allowBranchMismatch: params.allowBranchMismatch
|
|
9162
|
+
const baseline = await readLocalBaseline({
|
|
9163
|
+
repoFingerprint: binding.repoFingerprint,
|
|
9164
|
+
laneId: binding.laneId,
|
|
9165
|
+
repoRoot
|
|
9300
9166
|
});
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
throw new RemixError("Cannot record a no-diff turn while the worktree has local changes.", {
|
|
9167
|
+
if (!baseline) {
|
|
9168
|
+
throw new RemixError("Local Remix baseline is missing for this lane.", {
|
|
9304
9169
|
exitCode: 2,
|
|
9305
|
-
hint: "
|
|
9306
|
-
});
|
|
9307
|
-
}
|
|
9308
|
-
if (preflight.status === "ready_to_fast_forward") {
|
|
9309
|
-
await collabSync({
|
|
9310
|
-
api: params.api,
|
|
9311
|
-
cwd: repoRoot,
|
|
9312
|
-
dryRun: false,
|
|
9313
|
-
allowBranchMismatch: params.allowBranchMismatch
|
|
9170
|
+
hint: "Run `remix collab re-anchor` to create a fresh baseline."
|
|
9314
9171
|
});
|
|
9315
9172
|
}
|
|
9316
|
-
const
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
operation: "`remix collab record-turn`"
|
|
9173
|
+
const snapshot = await captureLocalSnapshot({
|
|
9174
|
+
repoRoot,
|
|
9175
|
+
repoFingerprint: binding.repoFingerprint,
|
|
9176
|
+
laneId: binding.laneId,
|
|
9177
|
+
branchName: binding.branchName
|
|
9322
9178
|
});
|
|
9323
|
-
const
|
|
9179
|
+
const mode = snapshot.snapshotHash === baseline.lastSnapshotHash ? "no_diff_turn" : "changed_turn";
|
|
9324
9180
|
const idempotencyKey = params.idempotencyKey?.trim() || buildDeterministicIdempotencyKey({
|
|
9181
|
+
kind: "collab_finalize_turn_boundary_v1",
|
|
9325
9182
|
appId: binding.currentAppId,
|
|
9326
|
-
|
|
9327
|
-
|
|
9183
|
+
laneId: binding.laneId,
|
|
9184
|
+
baselineSnapshotId: baseline.lastSnapshotId,
|
|
9185
|
+
baselineServerHeadHash: baseline.lastServerHeadHash,
|
|
9186
|
+
currentSnapshotId: snapshot.id,
|
|
9187
|
+
currentSnapshotHash: snapshot.snapshotHash,
|
|
9188
|
+
repoState: detected.repoState,
|
|
9328
9189
|
prompt,
|
|
9329
9190
|
assistantResponse
|
|
9330
9191
|
});
|
|
9331
|
-
const
|
|
9332
|
-
|
|
9333
|
-
|
|
9192
|
+
const job = await enqueueCapturedFinalizeTurn({
|
|
9193
|
+
repoRoot,
|
|
9194
|
+
repoFingerprint: binding.repoFingerprint,
|
|
9195
|
+
currentAppId: binding.currentAppId,
|
|
9196
|
+
laneId: binding.laneId,
|
|
9197
|
+
threadId: binding.threadId,
|
|
9198
|
+
branchName: binding.branchName,
|
|
9334
9199
|
prompt,
|
|
9335
9200
|
assistantResponse,
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9201
|
+
baselineSnapshotId: baseline.lastSnapshotId,
|
|
9202
|
+
baselineServerHeadHash: baseline.lastServerHeadHash,
|
|
9203
|
+
currentSnapshotId: snapshot.id,
|
|
9204
|
+
idempotencyKey,
|
|
9205
|
+
metadata: {
|
|
9340
9206
|
remoteUrl: binding.remoteUrl,
|
|
9341
9207
|
defaultBranch: binding.defaultBranch,
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9208
|
+
actor: params.actor ?? null,
|
|
9209
|
+
repoState: detected.repoState
|
|
9210
|
+
}
|
|
9345
9211
|
});
|
|
9346
|
-
|
|
9347
|
-
|
|
9212
|
+
return {
|
|
9213
|
+
mode,
|
|
9214
|
+
idempotencyKey,
|
|
9215
|
+
queued: true,
|
|
9216
|
+
jobId: job.id,
|
|
9217
|
+
repoState: detected.repoState,
|
|
9218
|
+
changeStep: null,
|
|
9219
|
+
collabTurn: null,
|
|
9220
|
+
autoSync: null,
|
|
9221
|
+
warnings: [FINALIZE_QUEUED_WARNING, ...collectWarnings(detected.warnings)]
|
|
9222
|
+
};
|
|
9348
9223
|
}
|
|
9349
9224
|
|
|
9350
|
-
// node_modules/@remixhq/core/dist/chunk-
|
|
9225
|
+
// node_modules/@remixhq/core/dist/chunk-R7FVSCQW.js
|
|
9351
9226
|
async function readJsonSafe(res) {
|
|
9352
9227
|
const ct = res.headers.get("content-type") ?? "";
|
|
9353
9228
|
if (!ct.toLowerCase().includes("application/json")) return null;
|
|
@@ -9361,7 +9236,7 @@ function createApiClient(config, opts) {
|
|
|
9361
9236
|
const apiKey = (opts?.apiKey ?? "").trim();
|
|
9362
9237
|
const tokenProvider = opts?.tokenProvider;
|
|
9363
9238
|
const CLIENT_KEY_HEADER = "x-comerge-api-key";
|
|
9364
|
-
async function request(
|
|
9239
|
+
async function request(path12, init) {
|
|
9365
9240
|
if (!tokenProvider) {
|
|
9366
9241
|
throw new RemixError("API client is missing a token provider.", {
|
|
9367
9242
|
exitCode: 1,
|
|
@@ -9369,7 +9244,7 @@ function createApiClient(config, opts) {
|
|
|
9369
9244
|
});
|
|
9370
9245
|
}
|
|
9371
9246
|
const auth = await tokenProvider();
|
|
9372
|
-
const url = new URL(
|
|
9247
|
+
const url = new URL(path12, config.apiUrl).toString();
|
|
9373
9248
|
const doFetch = async (bearer) => fetch(url, {
|
|
9374
9249
|
...init,
|
|
9375
9250
|
headers: {
|
|
@@ -9393,7 +9268,7 @@ function createApiClient(config, opts) {
|
|
|
9393
9268
|
const json = await readJsonSafe(res);
|
|
9394
9269
|
return json ?? null;
|
|
9395
9270
|
}
|
|
9396
|
-
async function requestBinary(
|
|
9271
|
+
async function requestBinary(path12, init) {
|
|
9397
9272
|
if (!tokenProvider) {
|
|
9398
9273
|
throw new RemixError("API client is missing a token provider.", {
|
|
9399
9274
|
exitCode: 1,
|
|
@@ -9401,7 +9276,7 @@ function createApiClient(config, opts) {
|
|
|
9401
9276
|
});
|
|
9402
9277
|
}
|
|
9403
9278
|
const auth = await tokenProvider();
|
|
9404
|
-
const url = new URL(
|
|
9279
|
+
const url = new URL(path12, config.apiUrl).toString();
|
|
9405
9280
|
const doFetch = async (bearer) => fetch(url, {
|
|
9406
9281
|
...init,
|
|
9407
9282
|
headers: {
|
|
@@ -9500,6 +9375,15 @@ function createApiClient(config, opts) {
|
|
|
9500
9375
|
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
9501
9376
|
return request(`/v1/apps/${encodeURIComponent(appId)}/edit-queue${suffix}`, { method: "GET" });
|
|
9502
9377
|
},
|
|
9378
|
+
listAppJobQueue: (appId, params) => {
|
|
9379
|
+
const qs = new URLSearchParams();
|
|
9380
|
+
if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
|
|
9381
|
+
if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
|
|
9382
|
+
for (const kind of params?.kind ?? []) qs.append("kind", kind);
|
|
9383
|
+
for (const status of params?.status ?? []) qs.append("status", status);
|
|
9384
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
9385
|
+
return request(`/v1/apps/${encodeURIComponent(appId)}/job-queue${suffix}`, { method: "GET" });
|
|
9386
|
+
},
|
|
9503
9387
|
getMergeRequest: (mrId) => request(`/v1/merge-requests/${encodeURIComponent(mrId)}`, { method: "GET" }),
|
|
9504
9388
|
presignImportUpload: (payload) => request("/v1/apps/import/upload/presign", { method: "POST", body: JSON.stringify(payload) }),
|
|
9505
9389
|
importFromUpload: (payload) => request("/v1/apps/import/upload", { method: "POST", body: JSON.stringify(payload) }),
|
|
@@ -9507,6 +9391,11 @@ function createApiClient(config, opts) {
|
|
|
9507
9391
|
importFromUploadFirstParty: (payload) => request("/v1/apps/import/upload/first-party", { method: "POST", body: JSON.stringify(payload) }),
|
|
9508
9392
|
importFromGithubFirstParty: (payload) => request("/v1/apps/import/github/first-party", { method: "POST", body: JSON.stringify(payload) }),
|
|
9509
9393
|
forkApp: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/fork`, { method: "POST", body: JSON.stringify(payload ?? {}) }),
|
|
9394
|
+
getAppHead: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/head`, { method: "GET" }),
|
|
9395
|
+
getAppDelta: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/delta`, {
|
|
9396
|
+
method: "POST",
|
|
9397
|
+
body: JSON.stringify(payload)
|
|
9398
|
+
}),
|
|
9510
9399
|
downloadAppBundle: (appId) => requestBinary(`/v1/apps/${encodeURIComponent(appId)}/download.bundle`, { method: "GET" }),
|
|
9511
9400
|
createChangeStep: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/change-steps`, {
|
|
9512
9401
|
method: "POST",
|
|
@@ -10219,8 +10108,8 @@ function getErrorMap() {
|
|
|
10219
10108
|
|
|
10220
10109
|
// node_modules/zod/v3/helpers/parseUtil.js
|
|
10221
10110
|
var makeIssue = (params) => {
|
|
10222
|
-
const { data, path:
|
|
10223
|
-
const fullPath = [...
|
|
10111
|
+
const { data, path: path12, errorMaps, issueData } = params;
|
|
10112
|
+
const fullPath = [...path12, ...issueData.path || []];
|
|
10224
10113
|
const fullIssue = {
|
|
10225
10114
|
...issueData,
|
|
10226
10115
|
path: fullPath
|
|
@@ -10336,11 +10225,11 @@ var errorUtil;
|
|
|
10336
10225
|
|
|
10337
10226
|
// node_modules/zod/v3/types.js
|
|
10338
10227
|
var ParseInputLazyPath = class {
|
|
10339
|
-
constructor(parent, value,
|
|
10228
|
+
constructor(parent, value, path12, key) {
|
|
10340
10229
|
this._cachedPath = [];
|
|
10341
10230
|
this.parent = parent;
|
|
10342
10231
|
this.data = value;
|
|
10343
|
-
this._path =
|
|
10232
|
+
this._path = path12;
|
|
10344
10233
|
this._key = key;
|
|
10345
10234
|
}
|
|
10346
10235
|
get path() {
|
|
@@ -13783,8 +13672,8 @@ var coerce = {
|
|
|
13783
13672
|
var NEVER = INVALID;
|
|
13784
13673
|
|
|
13785
13674
|
// node_modules/@remixhq/core/dist/chunk-EVWDYCBL.js
|
|
13786
|
-
var
|
|
13787
|
-
var
|
|
13675
|
+
var import_promises17 = __toESM(require("fs/promises"), 1);
|
|
13676
|
+
var import_os3 = __toESM(require("os"), 1);
|
|
13788
13677
|
var import_path7 = __toESM(require("path"), 1);
|
|
13789
13678
|
|
|
13790
13679
|
// node_modules/tslib/tslib.es6.mjs
|
|
@@ -22673,8 +22562,8 @@ var IcebergError = class extends Error {
|
|
|
22673
22562
|
return this.status === 419;
|
|
22674
22563
|
}
|
|
22675
22564
|
};
|
|
22676
|
-
function buildUrl(baseUrl,
|
|
22677
|
-
const url = new URL(
|
|
22565
|
+
function buildUrl(baseUrl, path12, query) {
|
|
22566
|
+
const url = new URL(path12, baseUrl);
|
|
22678
22567
|
if (query) {
|
|
22679
22568
|
for (const [key, value] of Object.entries(query)) {
|
|
22680
22569
|
if (value !== void 0) {
|
|
@@ -22704,12 +22593,12 @@ function createFetchClient(options) {
|
|
|
22704
22593
|
return {
|
|
22705
22594
|
async request({
|
|
22706
22595
|
method,
|
|
22707
|
-
path:
|
|
22596
|
+
path: path12,
|
|
22708
22597
|
query,
|
|
22709
22598
|
body,
|
|
22710
22599
|
headers
|
|
22711
22600
|
}) {
|
|
22712
|
-
const url = buildUrl(options.baseUrl,
|
|
22601
|
+
const url = buildUrl(options.baseUrl, path12, query);
|
|
22713
22602
|
const authHeaders = await buildAuthHeaders(options.auth);
|
|
22714
22603
|
const res = await fetchFn(url, {
|
|
22715
22604
|
method,
|
|
@@ -23528,7 +23417,7 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
23528
23417
|
* @param path The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload.
|
|
23529
23418
|
* @param fileBody The body of the file to be stored in the bucket.
|
|
23530
23419
|
*/
|
|
23531
|
-
async uploadOrUpdate(method,
|
|
23420
|
+
async uploadOrUpdate(method, path12, fileBody, fileOptions) {
|
|
23532
23421
|
var _this = this;
|
|
23533
23422
|
return _this.handleOperation(async () => {
|
|
23534
23423
|
let body;
|
|
@@ -23552,7 +23441,7 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
23552
23441
|
if ((typeof ReadableStream !== "undefined" && body instanceof ReadableStream || body && typeof body === "object" && "pipe" in body && typeof body.pipe === "function") && !options.duplex) options.duplex = "half";
|
|
23553
23442
|
}
|
|
23554
23443
|
if (fileOptions === null || fileOptions === void 0 ? void 0 : fileOptions.headers) headers = _objectSpread22(_objectSpread22({}, headers), fileOptions.headers);
|
|
23555
|
-
const cleanPath = _this._removeEmptyFolders(
|
|
23444
|
+
const cleanPath = _this._removeEmptyFolders(path12);
|
|
23556
23445
|
const _path = _this._getFinalPath(cleanPath);
|
|
23557
23446
|
const data = await (method == "PUT" ? put : post)(_this.fetch, `${_this.url}/object/${_path}`, body, _objectSpread22({ headers }, (options === null || options === void 0 ? void 0 : options.duplex) ? { duplex: options.duplex } : {}));
|
|
23558
23447
|
return {
|
|
@@ -23613,8 +23502,8 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
23613
23502
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
23614
23503
|
* - For React Native, using either `Blob`, `File` or `FormData` does not work as intended. Upload file using `ArrayBuffer` from base64 file data instead, see example below.
|
|
23615
23504
|
*/
|
|
23616
|
-
async upload(
|
|
23617
|
-
return this.uploadOrUpdate("POST",
|
|
23505
|
+
async upload(path12, fileBody, fileOptions) {
|
|
23506
|
+
return this.uploadOrUpdate("POST", path12, fileBody, fileOptions);
|
|
23618
23507
|
}
|
|
23619
23508
|
/**
|
|
23620
23509
|
* Upload a file with a token generated from `createSignedUploadUrl`.
|
|
@@ -23653,9 +23542,9 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
23653
23542
|
* - `objects` table permissions: none
|
|
23654
23543
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
23655
23544
|
*/
|
|
23656
|
-
async uploadToSignedUrl(
|
|
23545
|
+
async uploadToSignedUrl(path12, token, fileBody, fileOptions) {
|
|
23657
23546
|
var _this3 = this;
|
|
23658
|
-
const cleanPath = _this3._removeEmptyFolders(
|
|
23547
|
+
const cleanPath = _this3._removeEmptyFolders(path12);
|
|
23659
23548
|
const _path = _this3._getFinalPath(cleanPath);
|
|
23660
23549
|
const url = new URL(_this3.url + `/object/upload/sign/${_path}`);
|
|
23661
23550
|
url.searchParams.set("token", token);
|
|
@@ -23717,10 +23606,10 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
23717
23606
|
* - `objects` table permissions: `insert`
|
|
23718
23607
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
23719
23608
|
*/
|
|
23720
|
-
async createSignedUploadUrl(
|
|
23609
|
+
async createSignedUploadUrl(path12, options) {
|
|
23721
23610
|
var _this4 = this;
|
|
23722
23611
|
return _this4.handleOperation(async () => {
|
|
23723
|
-
let _path = _this4._getFinalPath(
|
|
23612
|
+
let _path = _this4._getFinalPath(path12);
|
|
23724
23613
|
const headers = _objectSpread22({}, _this4.headers);
|
|
23725
23614
|
if (options === null || options === void 0 ? void 0 : options.upsert) headers["x-upsert"] = "true";
|
|
23726
23615
|
const data = await post(_this4.fetch, `${_this4.url}/object/upload/sign/${_path}`, {}, { headers });
|
|
@@ -23729,7 +23618,7 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
23729
23618
|
if (!token) throw new StorageError("No token returned by API");
|
|
23730
23619
|
return {
|
|
23731
23620
|
signedUrl: url.toString(),
|
|
23732
|
-
path:
|
|
23621
|
+
path: path12,
|
|
23733
23622
|
token
|
|
23734
23623
|
};
|
|
23735
23624
|
});
|
|
@@ -23785,8 +23674,8 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
23785
23674
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
23786
23675
|
* - For React Native, using either `Blob`, `File` or `FormData` does not work as intended. Update file using `ArrayBuffer` from base64 file data instead, see example below.
|
|
23787
23676
|
*/
|
|
23788
|
-
async update(
|
|
23789
|
-
return this.uploadOrUpdate("PUT",
|
|
23677
|
+
async update(path12, fileBody, fileOptions) {
|
|
23678
|
+
return this.uploadOrUpdate("PUT", path12, fileBody, fileOptions);
|
|
23790
23679
|
}
|
|
23791
23680
|
/**
|
|
23792
23681
|
* Moves an existing file to a new path in the same bucket.
|
|
@@ -23933,10 +23822,10 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
23933
23822
|
* - `objects` table permissions: `select`
|
|
23934
23823
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
23935
23824
|
*/
|
|
23936
|
-
async createSignedUrl(
|
|
23825
|
+
async createSignedUrl(path12, expiresIn, options) {
|
|
23937
23826
|
var _this8 = this;
|
|
23938
23827
|
return _this8.handleOperation(async () => {
|
|
23939
|
-
let _path = _this8._getFinalPath(
|
|
23828
|
+
let _path = _this8._getFinalPath(path12);
|
|
23940
23829
|
const hasTransform = typeof (options === null || options === void 0 ? void 0 : options.transform) === "object" && options.transform !== null && Object.keys(options.transform).length > 0;
|
|
23941
23830
|
let data = await post(_this8.fetch, `${_this8.url}/object/sign/${_path}`, _objectSpread22({ expiresIn }, hasTransform ? { transform: options.transform } : {}), { headers: _this8.headers });
|
|
23942
23831
|
const downloadQueryParam = (options === null || options === void 0 ? void 0 : options.download) ? `&download=${options.download === true ? "" : options.download}` : "";
|
|
@@ -24063,11 +23952,11 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
24063
23952
|
* - `objects` table permissions: `select`
|
|
24064
23953
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
24065
23954
|
*/
|
|
24066
|
-
download(
|
|
23955
|
+
download(path12, options, parameters) {
|
|
24067
23956
|
const renderPath = typeof (options === null || options === void 0 ? void 0 : options.transform) !== "undefined" ? "render/image/authenticated" : "object";
|
|
24068
23957
|
const transformationQuery = this.transformOptsToQueryString((options === null || options === void 0 ? void 0 : options.transform) || {});
|
|
24069
23958
|
const queryString = transformationQuery ? `?${transformationQuery}` : "";
|
|
24070
|
-
const _path = this._getFinalPath(
|
|
23959
|
+
const _path = this._getFinalPath(path12);
|
|
24071
23960
|
const downloadFn = () => get(this.fetch, `${this.url}/${renderPath}/${_path}${queryString}`, {
|
|
24072
23961
|
headers: this.headers,
|
|
24073
23962
|
noResolveJson: true
|
|
@@ -24097,9 +23986,9 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
24097
23986
|
* }
|
|
24098
23987
|
* ```
|
|
24099
23988
|
*/
|
|
24100
|
-
async info(
|
|
23989
|
+
async info(path12) {
|
|
24101
23990
|
var _this10 = this;
|
|
24102
|
-
const _path = _this10._getFinalPath(
|
|
23991
|
+
const _path = _this10._getFinalPath(path12);
|
|
24103
23992
|
return _this10.handleOperation(async () => {
|
|
24104
23993
|
return recursiveToCamel(await get(_this10.fetch, `${_this10.url}/object/info/${_path}`, { headers: _this10.headers }));
|
|
24105
23994
|
});
|
|
@@ -24119,9 +24008,9 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
24119
24008
|
* .exists('folder/avatar1.png')
|
|
24120
24009
|
* ```
|
|
24121
24010
|
*/
|
|
24122
|
-
async exists(
|
|
24011
|
+
async exists(path12) {
|
|
24123
24012
|
var _this11 = this;
|
|
24124
|
-
const _path = _this11._getFinalPath(
|
|
24013
|
+
const _path = _this11._getFinalPath(path12);
|
|
24125
24014
|
try {
|
|
24126
24015
|
await head(_this11.fetch, `${_this11.url}/object/${_path}`, { headers: _this11.headers });
|
|
24127
24016
|
return {
|
|
@@ -24198,8 +24087,8 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
24198
24087
|
* - `objects` table permissions: none
|
|
24199
24088
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
24200
24089
|
*/
|
|
24201
|
-
getPublicUrl(
|
|
24202
|
-
const _path = this._getFinalPath(
|
|
24090
|
+
getPublicUrl(path12, options) {
|
|
24091
|
+
const _path = this._getFinalPath(path12);
|
|
24203
24092
|
const _queryString = [];
|
|
24204
24093
|
const downloadQueryParam = (options === null || options === void 0 ? void 0 : options.download) ? `download=${options.download === true ? "" : options.download}` : "";
|
|
24205
24094
|
if (downloadQueryParam !== "") _queryString.push(downloadQueryParam);
|
|
@@ -24338,10 +24227,10 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
24338
24227
|
* - `objects` table permissions: `select`
|
|
24339
24228
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
24340
24229
|
*/
|
|
24341
|
-
async list(
|
|
24230
|
+
async list(path12, options, parameters) {
|
|
24342
24231
|
var _this13 = this;
|
|
24343
24232
|
return _this13.handleOperation(async () => {
|
|
24344
|
-
const body = _objectSpread22(_objectSpread22(_objectSpread22({}, DEFAULT_SEARCH_OPTIONS), options), {}, { prefix:
|
|
24233
|
+
const body = _objectSpread22(_objectSpread22(_objectSpread22({}, DEFAULT_SEARCH_OPTIONS), options), {}, { prefix: path12 || "" });
|
|
24345
24234
|
return await post(_this13.fetch, `${_this13.url}/object/list/${_this13.bucketId}`, body, { headers: _this13.headers }, parameters);
|
|
24346
24235
|
});
|
|
24347
24236
|
}
|
|
@@ -24405,11 +24294,11 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
24405
24294
|
if (typeof Buffer !== "undefined") return Buffer.from(data).toString("base64");
|
|
24406
24295
|
return btoa(data);
|
|
24407
24296
|
}
|
|
24408
|
-
_getFinalPath(
|
|
24409
|
-
return `${this.bucketId}/${
|
|
24297
|
+
_getFinalPath(path12) {
|
|
24298
|
+
return `${this.bucketId}/${path12.replace(/^\/+/, "")}`;
|
|
24410
24299
|
}
|
|
24411
|
-
_removeEmptyFolders(
|
|
24412
|
-
return
|
|
24300
|
+
_removeEmptyFolders(path12) {
|
|
24301
|
+
return path12.replace(/^\/|\/$/g, "").replace(/\/+/g, "/");
|
|
24413
24302
|
}
|
|
24414
24303
|
transformOptsToQueryString(transform) {
|
|
24415
24304
|
const params = [];
|
|
@@ -26123,7 +26012,7 @@ function decodeJWT(token) {
|
|
|
26123
26012
|
};
|
|
26124
26013
|
return data;
|
|
26125
26014
|
}
|
|
26126
|
-
async function
|
|
26015
|
+
async function sleep2(time) {
|
|
26127
26016
|
return await new Promise((accept) => {
|
|
26128
26017
|
setTimeout(() => accept(null), time);
|
|
26129
26018
|
});
|
|
@@ -31892,7 +31781,7 @@ var GoTrueClient = class _GoTrueClient {
|
|
|
31892
31781
|
const startedAt = Date.now();
|
|
31893
31782
|
return await retryable(async (attempt) => {
|
|
31894
31783
|
if (attempt > 0) {
|
|
31895
|
-
await
|
|
31784
|
+
await sleep2(200 * Math.pow(2, attempt - 1));
|
|
31896
31785
|
}
|
|
31897
31786
|
this._debug(debugName, "refreshing attempt", attempt);
|
|
31898
31787
|
return await _request(this.fetch, "POST", `${this.url}/token?grant_type=refresh_token`, {
|
|
@@ -33444,7 +33333,7 @@ var storedSessionSchema = external_exports.object({
|
|
|
33444
33333
|
function xdgConfigHome() {
|
|
33445
33334
|
const value = process.env.XDG_CONFIG_HOME;
|
|
33446
33335
|
if (typeof value === "string" && value.trim()) return value;
|
|
33447
|
-
return import_path7.default.join(
|
|
33336
|
+
return import_path7.default.join(import_os3.default.homedir(), ".config");
|
|
33448
33337
|
}
|
|
33449
33338
|
async function maybeLoadKeytar() {
|
|
33450
33339
|
try {
|
|
@@ -33463,21 +33352,21 @@ async function maybeLoadKeytar() {
|
|
|
33463
33352
|
}
|
|
33464
33353
|
async function ensurePathPermissions(filePath) {
|
|
33465
33354
|
const dir = import_path7.default.dirname(filePath);
|
|
33466
|
-
await
|
|
33355
|
+
await import_promises17.default.mkdir(dir, { recursive: true });
|
|
33467
33356
|
try {
|
|
33468
|
-
await
|
|
33357
|
+
await import_promises17.default.chmod(dir, 448);
|
|
33469
33358
|
} catch {
|
|
33470
33359
|
}
|
|
33471
33360
|
try {
|
|
33472
|
-
await
|
|
33361
|
+
await import_promises17.default.chmod(filePath, 384);
|
|
33473
33362
|
} catch {
|
|
33474
33363
|
}
|
|
33475
33364
|
}
|
|
33476
33365
|
async function writeJsonAtomic2(filePath, value) {
|
|
33477
|
-
await
|
|
33366
|
+
await import_promises17.default.mkdir(import_path7.default.dirname(filePath), { recursive: true });
|
|
33478
33367
|
const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
33479
|
-
await
|
|
33480
|
-
await
|
|
33368
|
+
await import_promises17.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
33369
|
+
await import_promises17.default.rename(tmpPath, filePath);
|
|
33481
33370
|
}
|
|
33482
33371
|
async function writeSessionFileFallback(filePath, session) {
|
|
33483
33372
|
await writeJsonAtomic2(filePath, session);
|
|
@@ -33500,7 +33389,7 @@ function createLocalSessionStore(params) {
|
|
|
33500
33389
|
return null;
|
|
33501
33390
|
}
|
|
33502
33391
|
}
|
|
33503
|
-
const raw = await
|
|
33392
|
+
const raw = await import_promises17.default.readFile(filePath, "utf8").catch(() => null);
|
|
33504
33393
|
if (!raw) return null;
|
|
33505
33394
|
try {
|
|
33506
33395
|
const parsed = storedSessionSchema.safeParse(JSON.parse(raw));
|
|
@@ -33687,12 +33576,12 @@ async function createHookCollabApiClient() {
|
|
|
33687
33576
|
|
|
33688
33577
|
// src/hook-diagnostics.ts
|
|
33689
33578
|
var import_node_crypto2 = require("crypto");
|
|
33690
|
-
var
|
|
33579
|
+
var import_promises19 = __toESM(require("fs/promises"), 1);
|
|
33691
33580
|
var import_node_os5 = __toESM(require("os"), 1);
|
|
33692
33581
|
var import_node_path7 = __toESM(require("path"), 1);
|
|
33693
33582
|
|
|
33694
33583
|
// src/hook-state.ts
|
|
33695
|
-
var
|
|
33584
|
+
var import_promises18 = __toESM(require("fs/promises"), 1);
|
|
33696
33585
|
var import_node_os4 = __toESM(require("os"), 1);
|
|
33697
33586
|
var import_node_path6 = __toESM(require("path"), 1);
|
|
33698
33587
|
var import_node_crypto = require("crypto");
|
|
@@ -33710,20 +33599,20 @@ function stateLockMetaPath(sessionId) {
|
|
|
33710
33599
|
return import_node_path6.default.join(stateLockPath(sessionId), "owner.json");
|
|
33711
33600
|
}
|
|
33712
33601
|
async function writeJsonAtomic3(filePath, value) {
|
|
33713
|
-
await
|
|
33602
|
+
await import_promises18.default.mkdir(import_node_path6.default.dirname(filePath), { recursive: true });
|
|
33714
33603
|
const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
33715
|
-
await
|
|
33716
|
-
await
|
|
33604
|
+
await import_promises18.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
33605
|
+
await import_promises18.default.rename(tmpPath, filePath);
|
|
33717
33606
|
}
|
|
33718
33607
|
var STATE_LOCK_WAIT_MS = 2e3;
|
|
33719
33608
|
var STATE_LOCK_POLL_MS = 25;
|
|
33720
33609
|
var STATE_LOCK_STALE_MS = 3e4;
|
|
33721
33610
|
var STATE_LOCK_HEARTBEAT_MS = 5e3;
|
|
33722
|
-
async function
|
|
33611
|
+
async function sleep3(ms) {
|
|
33723
33612
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
33724
33613
|
}
|
|
33725
33614
|
async function readStateLockMetadata(sessionId) {
|
|
33726
|
-
const raw = await
|
|
33615
|
+
const raw = await import_promises18.default.readFile(stateLockMetaPath(sessionId), "utf8").catch(() => null);
|
|
33727
33616
|
if (!raw) return null;
|
|
33728
33617
|
try {
|
|
33729
33618
|
const parsed = JSON.parse(raw);
|
|
@@ -33748,13 +33637,13 @@ async function tryRemoveStaleStateLock(sessionId) {
|
|
|
33748
33637
|
const metadata = await readStateLockMetadata(sessionId);
|
|
33749
33638
|
const staleByHeartbeat = metadata && Date.now() - new Date(metadata.heartbeatAt).getTime() > STATE_LOCK_STALE_MS;
|
|
33750
33639
|
if (staleByHeartbeat) {
|
|
33751
|
-
await
|
|
33640
|
+
await import_promises18.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
|
|
33752
33641
|
return true;
|
|
33753
33642
|
}
|
|
33754
33643
|
if (!metadata) {
|
|
33755
|
-
const lockStat = await
|
|
33644
|
+
const lockStat = await import_promises18.default.stat(lockPath).catch(() => null);
|
|
33756
33645
|
if (lockStat && Date.now() - lockStat.mtimeMs > STATE_LOCK_STALE_MS) {
|
|
33757
|
-
await
|
|
33646
|
+
await import_promises18.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
|
|
33758
33647
|
return true;
|
|
33759
33648
|
}
|
|
33760
33649
|
}
|
|
@@ -33763,10 +33652,10 @@ async function tryRemoveStaleStateLock(sessionId) {
|
|
|
33763
33652
|
async function acquireStateLock(sessionId) {
|
|
33764
33653
|
const lockPath = stateLockPath(sessionId);
|
|
33765
33654
|
const deadline = Date.now() + STATE_LOCK_WAIT_MS;
|
|
33766
|
-
await
|
|
33655
|
+
await import_promises18.default.mkdir(stateRoot(), { recursive: true });
|
|
33767
33656
|
while (true) {
|
|
33768
33657
|
try {
|
|
33769
|
-
await
|
|
33658
|
+
await import_promises18.default.mkdir(lockPath);
|
|
33770
33659
|
const ownerId = (0, import_node_crypto.randomUUID)();
|
|
33771
33660
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
33772
33661
|
const metadata = {
|
|
@@ -33791,7 +33680,7 @@ async function acquireStateLock(sessionId) {
|
|
|
33791
33680
|
clearInterval(heartbeat);
|
|
33792
33681
|
const currentMetadata = await readStateLockMetadata(sessionId);
|
|
33793
33682
|
if (currentMetadata?.ownerId === ownerId) {
|
|
33794
|
-
await
|
|
33683
|
+
await import_promises18.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
|
|
33795
33684
|
}
|
|
33796
33685
|
};
|
|
33797
33686
|
} catch (error) {
|
|
@@ -33805,7 +33694,7 @@ async function acquireStateLock(sessionId) {
|
|
|
33805
33694
|
if (Date.now() >= deadline) {
|
|
33806
33695
|
throw new Error(`Timed out acquiring hook state lock for session ${sessionId}.`);
|
|
33807
33696
|
}
|
|
33808
|
-
await
|
|
33697
|
+
await sleep3(STATE_LOCK_POLL_MS);
|
|
33809
33698
|
}
|
|
33810
33699
|
}
|
|
33811
33700
|
}
|
|
@@ -33831,6 +33720,12 @@ function normalizeStringArray(value) {
|
|
|
33831
33720
|
)
|
|
33832
33721
|
);
|
|
33833
33722
|
}
|
|
33723
|
+
function normalizeManualRecordingScope(value) {
|
|
33724
|
+
if (value === "full_turn") {
|
|
33725
|
+
return "full_turn";
|
|
33726
|
+
}
|
|
33727
|
+
return null;
|
|
33728
|
+
}
|
|
33834
33729
|
function normalizeTouchedRepo(value, repoRoot) {
|
|
33835
33730
|
if (!value || typeof value !== "object") return null;
|
|
33836
33731
|
const parsed = value;
|
|
@@ -33849,7 +33744,7 @@ function normalizeTouchedRepo(value, repoRoot) {
|
|
|
33849
33744
|
manuallyRecorded: Boolean(parsed.manuallyRecorded),
|
|
33850
33745
|
manuallyRecordedAt: normalizeString(parsed.manuallyRecordedAt),
|
|
33851
33746
|
manuallyRecordedByTool: normalizeString(parsed.manuallyRecordedByTool),
|
|
33852
|
-
manualRecordingScope: parsed.manualRecordingScope
|
|
33747
|
+
manualRecordingScope: normalizeManualRecordingScope(parsed.manualRecordingScope),
|
|
33853
33748
|
manualRemoteChangeRecordedAt: normalizeString(parsed.manualRemoteChangeRecordedAt),
|
|
33854
33749
|
stopAttempted: Boolean(parsed.stopAttempted),
|
|
33855
33750
|
stopRecorded: Boolean(parsed.stopRecorded),
|
|
@@ -33903,7 +33798,7 @@ async function updatePendingTurnState(sessionId, updater) {
|
|
|
33903
33798
|
});
|
|
33904
33799
|
}
|
|
33905
33800
|
async function loadPendingTurnState(sessionId) {
|
|
33906
|
-
const raw = await
|
|
33801
|
+
const raw = await import_promises18.default.readFile(statePath(sessionId), "utf8").catch(() => null);
|
|
33907
33802
|
if (!raw) return null;
|
|
33908
33803
|
try {
|
|
33909
33804
|
const parsed = JSON.parse(raw);
|
|
@@ -34005,14 +33900,14 @@ async function listTouchedRepos(sessionId) {
|
|
|
34005
33900
|
}
|
|
34006
33901
|
async function clearPendingTurnState(sessionId) {
|
|
34007
33902
|
await withStateLock(sessionId, async () => {
|
|
34008
|
-
await
|
|
33903
|
+
await import_promises18.default.rm(statePath(sessionId), { force: true }).catch(() => void 0);
|
|
34009
33904
|
});
|
|
34010
33905
|
}
|
|
34011
33906
|
|
|
34012
33907
|
// package.json
|
|
34013
33908
|
var package_default = {
|
|
34014
33909
|
name: "@remixhq/claude-plugin",
|
|
34015
|
-
version: "0.1.
|
|
33910
|
+
version: "0.1.18",
|
|
34016
33911
|
description: "Claude Code plugin for Remix collaboration workflows",
|
|
34017
33912
|
homepage: "https://github.com/RemixDotOne/remix-claude-plugin",
|
|
34018
33913
|
license: "MIT",
|
|
@@ -34043,8 +33938,8 @@ var package_default = {
|
|
|
34043
33938
|
prepack: "npm run build"
|
|
34044
33939
|
},
|
|
34045
33940
|
dependencies: {
|
|
34046
|
-
"@remixhq/core": "^0.1.
|
|
34047
|
-
"@remixhq/mcp": "^0.1.
|
|
33941
|
+
"@remixhq/core": "^0.1.13",
|
|
33942
|
+
"@remixhq/mcp": "^0.1.13"
|
|
34048
33943
|
},
|
|
34049
33944
|
devDependencies: {
|
|
34050
33945
|
"@types/node": "^25.4.0",
|
|
@@ -34095,13 +33990,13 @@ function normalizeFields(fields) {
|
|
|
34095
33990
|
return Object.fromEntries(normalizedEntries);
|
|
34096
33991
|
}
|
|
34097
33992
|
async function rotateLogIfNeeded(logPath) {
|
|
34098
|
-
const stat = await
|
|
33993
|
+
const stat = await import_promises19.default.stat(logPath).catch(() => null);
|
|
34099
33994
|
if (!stat || stat.size < MAX_LOG_BYTES) {
|
|
34100
33995
|
return;
|
|
34101
33996
|
}
|
|
34102
33997
|
const rotatedPath = `${logPath}.1`;
|
|
34103
|
-
await
|
|
34104
|
-
await
|
|
33998
|
+
await import_promises19.default.rm(rotatedPath, { force: true }).catch(() => void 0);
|
|
33999
|
+
await import_promises19.default.rename(logPath, rotatedPath).catch(() => void 0);
|
|
34105
34000
|
}
|
|
34106
34001
|
function summarizeText(value) {
|
|
34107
34002
|
if (typeof value !== "string" || !value.trim()) {
|
|
@@ -34121,7 +34016,7 @@ function summarizeText(value) {
|
|
|
34121
34016
|
async function appendHookDiagnosticsEvent(params) {
|
|
34122
34017
|
try {
|
|
34123
34018
|
const logPath = getHookDiagnosticsLogPath();
|
|
34124
|
-
await
|
|
34019
|
+
await import_promises19.default.mkdir(import_node_path7.default.dirname(logPath), { recursive: true });
|
|
34125
34020
|
await rotateLogIfNeeded(logPath);
|
|
34126
34021
|
const event = {
|
|
34127
34022
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -34138,14 +34033,14 @@ async function appendHookDiagnosticsEvent(params) {
|
|
|
34138
34033
|
message: params.message?.trim() || null,
|
|
34139
34034
|
fields: normalizeFields(params.fields)
|
|
34140
34035
|
};
|
|
34141
|
-
await
|
|
34036
|
+
await import_promises19.default.appendFile(logPath, `${JSON.stringify(event)}
|
|
34142
34037
|
`, "utf8");
|
|
34143
34038
|
} catch {
|
|
34144
34039
|
}
|
|
34145
34040
|
}
|
|
34146
34041
|
|
|
34147
34042
|
// src/hook-utils.ts
|
|
34148
|
-
var
|
|
34043
|
+
var import_promises20 = __toESM(require("fs/promises"), 1);
|
|
34149
34044
|
var import_node_path8 = __toESM(require("path"), 1);
|
|
34150
34045
|
async function readJsonStdin() {
|
|
34151
34046
|
const chunks = [];
|
|
@@ -34209,13 +34104,13 @@ function extractBoolean(input, keys) {
|
|
|
34209
34104
|
async function findBoundRepo(startPath) {
|
|
34210
34105
|
if (!startPath) return null;
|
|
34211
34106
|
let current = import_node_path8.default.resolve(startPath);
|
|
34212
|
-
let stats = await
|
|
34107
|
+
let stats = await import_promises20.default.stat(current).catch(() => null);
|
|
34213
34108
|
if (stats?.isFile()) {
|
|
34214
34109
|
current = import_node_path8.default.dirname(current);
|
|
34215
34110
|
}
|
|
34216
34111
|
while (true) {
|
|
34217
34112
|
const bindingPath = import_node_path8.default.join(current, ".remix", "config.json");
|
|
34218
|
-
const bindingStats = await
|
|
34113
|
+
const bindingStats = await import_promises20.default.stat(bindingPath).catch(() => null);
|
|
34219
34114
|
if (bindingStats?.isFile()) return current;
|
|
34220
34115
|
const parent = import_node_path8.default.dirname(current);
|
|
34221
34116
|
if (parent === current) return null;
|
|
@@ -34242,6 +34137,7 @@ var HOOK_ACTOR = {
|
|
|
34242
34137
|
version: pluginMetadata.version,
|
|
34243
34138
|
provider: "anthropic"
|
|
34244
34139
|
};
|
|
34140
|
+
var collabFinalizeTurn2 = collabFinalizeTurn;
|
|
34245
34141
|
function getErrorDetails(error) {
|
|
34246
34142
|
if (error instanceof Error) {
|
|
34247
34143
|
const hint = typeof error.hint === "string" ? String(error.hint) : null;
|
|
@@ -34253,52 +34149,13 @@ function getErrorDetails(error) {
|
|
|
34253
34149
|
const message = typeof error === "string" && error.trim() ? error.trim() : "Fallback Remix turn recording failed.";
|
|
34254
34150
|
return { message, hint: null };
|
|
34255
34151
|
}
|
|
34256
|
-
function getRecordingBlockedMessage(status, repoRoot) {
|
|
34257
|
-
if (status.status === "branch_binding_missing") {
|
|
34258
|
-
return {
|
|
34259
|
-
message: "Fallback Remix turn recording was blocked because the current branch does not have a Remix lane binding yet.",
|
|
34260
|
-
hint: status.hint || `Run \`remix_collab_status\` for ${repoRoot}, then initialize or provision the current branch lane before recording work.`
|
|
34261
|
-
};
|
|
34262
|
-
}
|
|
34263
|
-
switch (status.status) {
|
|
34264
|
-
case "not_git_repo":
|
|
34265
|
-
return {
|
|
34266
|
-
message: "Fallback Remix turn recording failed because the repository is no longer inside a git repository.",
|
|
34267
|
-
hint: status.hint || `Repo root: ${repoRoot}`
|
|
34268
|
-
};
|
|
34269
|
-
case "not_bound":
|
|
34270
|
-
return {
|
|
34271
|
-
message: "Fallback Remix turn recording failed because the repository is no longer bound to Remix.",
|
|
34272
|
-
hint: status.hint || `Repo root: ${repoRoot}`
|
|
34273
|
-
};
|
|
34274
|
-
case "missing_head":
|
|
34275
|
-
return {
|
|
34276
|
-
message: "Fallback Remix turn recording failed because the repository HEAD could not be resolved.",
|
|
34277
|
-
hint: status.hint || `Repo root: ${repoRoot}`
|
|
34278
|
-
};
|
|
34279
|
-
case "branch_mismatch":
|
|
34280
|
-
return {
|
|
34281
|
-
message: "Fallback Remix turn recording was blocked because the current checkout branch does not match the branch expected by the bound Remix lane.",
|
|
34282
|
-
hint: status.hint || `Run \`remix_collab_status\` for ${repoRoot}, then switch back to the expected branch or refresh this branch's binding before recording or syncing.`
|
|
34283
|
-
};
|
|
34284
|
-
case "metadata_conflict":
|
|
34285
|
-
return {
|
|
34286
|
-
message: "Fallback Remix turn recording was blocked because local repository metadata conflicts with the bound Remix app.",
|
|
34287
|
-
hint: status.hint || `Repo root: ${repoRoot}`
|
|
34288
|
-
};
|
|
34289
|
-
case "reconcile_required":
|
|
34290
|
-
return {
|
|
34291
|
-
message: "Fallback Remix turn recording was blocked because the repository must be reconciled before recording can continue safely.",
|
|
34292
|
-
hint: status.hint || `Repo root: ${repoRoot}`
|
|
34293
|
-
};
|
|
34294
|
-
default:
|
|
34295
|
-
return null;
|
|
34296
|
-
}
|
|
34297
|
-
}
|
|
34298
34152
|
function buildRepoIdempotencyKey(turnId, repo) {
|
|
34299
34153
|
const repoToken = repo.currentAppId?.trim() || repo.repoRoot;
|
|
34300
34154
|
return `${turnId}:${repoToken}:finalize_turn`;
|
|
34301
34155
|
}
|
|
34156
|
+
function isLegacyManualRecordingTool(toolName) {
|
|
34157
|
+
return /remix_collab_(add|add_change_step|record_turn|record_no_diff_turn)$/i.test(toolName ?? "");
|
|
34158
|
+
}
|
|
34302
34159
|
function shouldSkipStopRecording(repo) {
|
|
34303
34160
|
if (repo.stopRecorded) {
|
|
34304
34161
|
return true;
|
|
@@ -34306,7 +34163,8 @@ function shouldSkipStopRecording(repo) {
|
|
|
34306
34163
|
if (!repo.manuallyRecorded) {
|
|
34307
34164
|
return false;
|
|
34308
34165
|
}
|
|
34309
|
-
|
|
34166
|
+
const alreadyRecordedByCompatibleFlow = repo.manualRecordingScope === "full_turn" || isLegacyManualRecordingTool(repo.manuallyRecordedByTool);
|
|
34167
|
+
if (!alreadyRecordedByCompatibleFlow) {
|
|
34310
34168
|
return false;
|
|
34311
34169
|
}
|
|
34312
34170
|
if (!repo.manuallyRecordedAt) {
|
|
@@ -34317,12 +34175,6 @@ function shouldSkipStopRecording(repo) {
|
|
|
34317
34175
|
}
|
|
34318
34176
|
return new Date(repo.lastObservedWriteAt).getTime() <= new Date(repo.manuallyRecordedAt).getTime();
|
|
34319
34177
|
}
|
|
34320
|
-
function hasNewObservedWriteSince(timestamp, repo) {
|
|
34321
|
-
if (!repo.lastObservedWriteAt) {
|
|
34322
|
-
return false;
|
|
34323
|
-
}
|
|
34324
|
-
return new Date(repo.lastObservedWriteAt).getTime() > new Date(timestamp).getTime();
|
|
34325
|
-
}
|
|
34326
34178
|
function createFallbackTouchedRepo(params) {
|
|
34327
34179
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
34328
34180
|
return {
|
|
@@ -34381,113 +34233,9 @@ async function recordTouchedRepo(params) {
|
|
|
34381
34233
|
reason: "repo_not_bound",
|
|
34382
34234
|
repoRoot: repo.repoRoot
|
|
34383
34235
|
});
|
|
34384
|
-
return false;
|
|
34385
|
-
}
|
|
34386
|
-
const workspaceDiff = await getWorkspaceDiff(repo.repoRoot);
|
|
34387
|
-
if (workspaceDiff.diff.trim()) {
|
|
34388
|
-
await appendHookDiagnosticsEvent({
|
|
34389
|
-
hook,
|
|
34390
|
-
sessionId,
|
|
34391
|
-
turnId,
|
|
34392
|
-
stage: "workspace_diff_checked",
|
|
34393
|
-
result: "info",
|
|
34394
|
-
repoRoot: repo.repoRoot,
|
|
34395
|
-
fields: {
|
|
34396
|
-
hasWorkspaceDiff: true,
|
|
34397
|
-
diffLength: workspaceDiff.diff.length
|
|
34398
|
-
}
|
|
34399
|
-
});
|
|
34400
|
-
if (repo.manualRemoteChangeRecordedAt && !hasNewObservedWriteSince(repo.manualRemoteChangeRecordedAt, repo)) {
|
|
34401
|
-
await markTouchedRepoStopRecorded(sessionId, repo.repoRoot, { mode: "changed_turn" });
|
|
34402
|
-
await appendHookDiagnosticsEvent({
|
|
34403
|
-
hook,
|
|
34404
|
-
sessionId,
|
|
34405
|
-
turnId,
|
|
34406
|
-
stage: "recording_skipped",
|
|
34407
|
-
result: "success",
|
|
34408
|
-
reason: "manual_recording_already_covers_diff",
|
|
34409
|
-
repoRoot: repo.repoRoot
|
|
34410
|
-
});
|
|
34411
|
-
return true;
|
|
34412
|
-
}
|
|
34413
|
-
const recordingPreflight2 = await collabRecordingPreflight({
|
|
34414
|
-
api,
|
|
34415
|
-
cwd: repo.repoRoot
|
|
34416
|
-
});
|
|
34417
|
-
const blocked2 = getRecordingBlockedMessage(recordingPreflight2, repo.repoRoot);
|
|
34418
|
-
if (blocked2) {
|
|
34419
|
-
await markTouchedRepoRecordingFailure(sessionId, repo.repoRoot, blocked2);
|
|
34420
|
-
await appendHookDiagnosticsEvent({
|
|
34421
|
-
hook,
|
|
34422
|
-
sessionId,
|
|
34423
|
-
turnId,
|
|
34424
|
-
stage: "recording_preflight",
|
|
34425
|
-
result: "error",
|
|
34426
|
-
reason: recordingPreflight2.status,
|
|
34427
|
-
repoRoot: repo.repoRoot,
|
|
34428
|
-
message: blocked2.message,
|
|
34429
|
-
fields: {
|
|
34430
|
-
hint: blocked2.hint
|
|
34431
|
-
}
|
|
34432
|
-
});
|
|
34433
|
-
return false;
|
|
34434
|
-
}
|
|
34435
|
-
await collabAdd({
|
|
34436
|
-
api,
|
|
34437
|
-
cwd: repo.repoRoot,
|
|
34438
|
-
prompt,
|
|
34439
|
-
assistantResponse,
|
|
34440
|
-
diffSource: "worktree",
|
|
34441
|
-
idempotencyKey: buildRepoIdempotencyKey(turnId, repo),
|
|
34442
|
-
actor: HOOK_ACTOR
|
|
34443
|
-
});
|
|
34444
|
-
await markTouchedRepoStopRecorded(sessionId, repo.repoRoot, { mode: "changed_turn" });
|
|
34445
|
-
await appendHookDiagnosticsEvent({
|
|
34446
|
-
hook,
|
|
34447
|
-
sessionId,
|
|
34448
|
-
turnId,
|
|
34449
|
-
stage: "recording_completed",
|
|
34450
|
-
result: "success",
|
|
34451
|
-
reason: "changed_turn_recorded",
|
|
34452
|
-
repoRoot: repo.repoRoot
|
|
34453
|
-
});
|
|
34454
|
-
return true;
|
|
34455
|
-
}
|
|
34456
|
-
await appendHookDiagnosticsEvent({
|
|
34457
|
-
hook,
|
|
34458
|
-
sessionId,
|
|
34459
|
-
turnId,
|
|
34460
|
-
stage: "workspace_diff_checked",
|
|
34461
|
-
result: "info",
|
|
34462
|
-
repoRoot: repo.repoRoot,
|
|
34463
|
-
fields: {
|
|
34464
|
-
hasWorkspaceDiff: false,
|
|
34465
|
-
diffLength: 0
|
|
34466
|
-
}
|
|
34467
|
-
});
|
|
34468
|
-
const recordingPreflight = await collabRecordingPreflight({
|
|
34469
|
-
api,
|
|
34470
|
-
cwd: repo.repoRoot
|
|
34471
|
-
});
|
|
34472
|
-
const blocked = getRecordingBlockedMessage(recordingPreflight, repo.repoRoot);
|
|
34473
|
-
if (blocked) {
|
|
34474
|
-
await markTouchedRepoRecordingFailure(sessionId, repo.repoRoot, blocked);
|
|
34475
|
-
await appendHookDiagnosticsEvent({
|
|
34476
|
-
hook,
|
|
34477
|
-
sessionId,
|
|
34478
|
-
turnId,
|
|
34479
|
-
stage: "recording_preflight",
|
|
34480
|
-
result: "error",
|
|
34481
|
-
reason: recordingPreflight.status,
|
|
34482
|
-
repoRoot: repo.repoRoot,
|
|
34483
|
-
message: blocked.message,
|
|
34484
|
-
fields: {
|
|
34485
|
-
hint: blocked.hint
|
|
34486
|
-
}
|
|
34487
|
-
});
|
|
34488
|
-
return false;
|
|
34236
|
+
return { recorded: false, queued: false };
|
|
34489
34237
|
}
|
|
34490
|
-
await
|
|
34238
|
+
const result = await collabFinalizeTurn2({
|
|
34491
34239
|
api,
|
|
34492
34240
|
cwd: repo.repoRoot,
|
|
34493
34241
|
prompt,
|
|
@@ -34495,17 +34243,17 @@ async function recordTouchedRepo(params) {
|
|
|
34495
34243
|
idempotencyKey: buildRepoIdempotencyKey(turnId, repo),
|
|
34496
34244
|
actor: HOOK_ACTOR
|
|
34497
34245
|
});
|
|
34498
|
-
await markTouchedRepoStopRecorded(sessionId, repo.repoRoot, { mode:
|
|
34246
|
+
await markTouchedRepoStopRecorded(sessionId, repo.repoRoot, { mode: result.mode });
|
|
34499
34247
|
await appendHookDiagnosticsEvent({
|
|
34500
34248
|
hook,
|
|
34501
34249
|
sessionId,
|
|
34502
34250
|
turnId,
|
|
34503
34251
|
stage: "recording_completed",
|
|
34504
34252
|
result: "success",
|
|
34505
|
-
reason:
|
|
34253
|
+
reason: result.mode,
|
|
34506
34254
|
repoRoot: repo.repoRoot
|
|
34507
34255
|
});
|
|
34508
|
-
return true;
|
|
34256
|
+
return { recorded: true, queued: result.queued === true };
|
|
34509
34257
|
} catch (error) {
|
|
34510
34258
|
const details = getErrorDetails(error);
|
|
34511
34259
|
await markTouchedRepoRecordingFailure(sessionId, repo.repoRoot, details);
|
|
@@ -34522,9 +34270,19 @@ async function recordTouchedRepo(params) {
|
|
|
34522
34270
|
hint: details.hint
|
|
34523
34271
|
}
|
|
34524
34272
|
});
|
|
34525
|
-
return false;
|
|
34273
|
+
return { recorded: false, queued: false };
|
|
34526
34274
|
}
|
|
34527
34275
|
}
|
|
34276
|
+
function spawnFinalizeQueueDrainer() {
|
|
34277
|
+
const entrypoint = process.argv[1];
|
|
34278
|
+
if (!entrypoint) return;
|
|
34279
|
+
const child = (0, import_node_child_process6.spawn)(process.execPath, [...process.execArgv, entrypoint, "--drain-finalize-queue"], {
|
|
34280
|
+
detached: true,
|
|
34281
|
+
stdio: "ignore",
|
|
34282
|
+
env: process.env
|
|
34283
|
+
});
|
|
34284
|
+
child.unref();
|
|
34285
|
+
}
|
|
34528
34286
|
async function runHookStopCollab(payload) {
|
|
34529
34287
|
const hook = "Stop";
|
|
34530
34288
|
if (extractBoolean(payload, ["stop_hook_active"])) {
|
|
@@ -34671,8 +34429,11 @@ async function runHookStopCollab(payload) {
|
|
|
34671
34429
|
}
|
|
34672
34430
|
});
|
|
34673
34431
|
let hadFailure = false;
|
|
34432
|
+
let queuedFinalizeWork = false;
|
|
34674
34433
|
for (const repo of touchedRepos) {
|
|
34675
34434
|
if (shouldSkipStopRecording(repo)) {
|
|
34435
|
+
const backupDrainQueued = repo.manuallyRecordedByTool === "remix_collab_finalize_turn" && repo.manualRecordingScope === "full_turn";
|
|
34436
|
+
queuedFinalizeWork = queuedFinalizeWork || backupDrainQueued;
|
|
34676
34437
|
await appendHookDiagnosticsEvent({
|
|
34677
34438
|
hook,
|
|
34678
34439
|
sessionId,
|
|
@@ -34683,12 +34444,13 @@ async function runHookStopCollab(payload) {
|
|
|
34683
34444
|
repoRoot: repo.repoRoot,
|
|
34684
34445
|
fields: {
|
|
34685
34446
|
manuallyRecorded: repo.manuallyRecorded,
|
|
34686
|
-
stopRecorded: repo.stopRecorded
|
|
34447
|
+
stopRecorded: repo.stopRecorded,
|
|
34448
|
+
backupDrainQueued
|
|
34687
34449
|
}
|
|
34688
34450
|
});
|
|
34689
34451
|
continue;
|
|
34690
34452
|
}
|
|
34691
|
-
const
|
|
34453
|
+
const recording = await recordTouchedRepo({
|
|
34692
34454
|
hook,
|
|
34693
34455
|
sessionId,
|
|
34694
34456
|
turnId: state.turnId,
|
|
@@ -34697,10 +34459,14 @@ async function runHookStopCollab(payload) {
|
|
|
34697
34459
|
assistantResponse,
|
|
34698
34460
|
api
|
|
34699
34461
|
});
|
|
34700
|
-
|
|
34462
|
+
queuedFinalizeWork = queuedFinalizeWork || recording.queued;
|
|
34463
|
+
if (!recording.recorded) {
|
|
34701
34464
|
hadFailure = true;
|
|
34702
34465
|
}
|
|
34703
34466
|
}
|
|
34467
|
+
if (queuedFinalizeWork) {
|
|
34468
|
+
spawnFinalizeQueueDrainer();
|
|
34469
|
+
}
|
|
34704
34470
|
if (!hadFailure) {
|
|
34705
34471
|
await clearPendingTurnState(sessionId);
|
|
34706
34472
|
await appendHookDiagnosticsEvent({
|
|
@@ -34739,6 +34505,14 @@ async function runHookStopCollab(payload) {
|
|
|
34739
34505
|
}
|
|
34740
34506
|
}
|
|
34741
34507
|
async function main() {
|
|
34508
|
+
if (process.argv.includes("--drain-finalize-queue")) {
|
|
34509
|
+
const api = await createHookCollabApiClient();
|
|
34510
|
+
const drainPendingFinalizeQueue2 = drainPendingFinalizeQueue;
|
|
34511
|
+
if (typeof drainPendingFinalizeQueue2 === "function") {
|
|
34512
|
+
await drainPendingFinalizeQueue2({ api });
|
|
34513
|
+
}
|
|
34514
|
+
return;
|
|
34515
|
+
}
|
|
34742
34516
|
const payload = await readJsonStdin();
|
|
34743
34517
|
await runHookStopCollab(payload);
|
|
34744
34518
|
}
|