@remixhq/claude-plugin 0.1.17 → 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 +18 -62
- 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 +1343 -1674
- 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 +2682 -1498
- 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 {
|
|
@@ -7872,7 +7461,7 @@ async function readCollabBindingState(repoRoot, options) {
|
|
|
7872
7461
|
try {
|
|
7873
7462
|
const persist = options?.persist === true;
|
|
7874
7463
|
const filePath = getCollabBindingPath(repoRoot);
|
|
7875
|
-
const raw = await
|
|
7464
|
+
const raw = await import_promises12.default.readFile(filePath, "utf8");
|
|
7876
7465
|
const parsed = JSON.parse(raw);
|
|
7877
7466
|
if (!parsed || typeof parsed !== "object") return null;
|
|
7878
7467
|
const currentBranch = normalizeBranchName(await getCurrentBranch(repoRoot).catch(() => null));
|
|
@@ -8072,15 +7661,19 @@ async function writeCollabBinding(repoRoot, binding) {
|
|
|
8072
7661
|
}
|
|
8073
7662
|
|
|
8074
7663
|
// node_modules/@remixhq/core/dist/collab.js
|
|
8075
|
-
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);
|
|
8076
7668
|
var import_path4 = __toESM(require("path"), 1);
|
|
8077
7669
|
var import_crypto2 = require("crypto");
|
|
8078
|
-
var
|
|
7670
|
+
var import_promises15 = __toESM(require("fs/promises"), 1);
|
|
8079
7671
|
var import_os2 = __toESM(require("os"), 1);
|
|
8080
7672
|
var import_path5 = __toESM(require("path"), 1);
|
|
8081
|
-
var
|
|
8082
|
-
var
|
|
7673
|
+
var import_crypto3 = require("crypto");
|
|
7674
|
+
var import_promises16 = __toESM(require("fs/promises"), 1);
|
|
8083
7675
|
var import_path6 = __toESM(require("path"), 1);
|
|
7676
|
+
var import_crypto4 = require("crypto");
|
|
8084
7677
|
function describeBranch(value) {
|
|
8085
7678
|
const normalized = String(value ?? "").trim();
|
|
8086
7679
|
return normalized || "(detached)";
|
|
@@ -8099,18 +7692,562 @@ function buildBranchMismatchHint(params) {
|
|
|
8099
7692
|
`Switch to ${describeBranch(params.branchName)} or rerun with ${overrideFlag} if this is intentional.`
|
|
8100
7693
|
].join("\n");
|
|
8101
7694
|
}
|
|
8102
|
-
function
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
|
|
8112
|
-
|
|
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
|
|
8113
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);
|
|
8114
8251
|
}
|
|
8115
8252
|
function unwrapResponseObject(resp, label) {
|
|
8116
8253
|
const obj = resp?.responseObject;
|
|
@@ -8124,7 +8261,7 @@ function sleep(ms) {
|
|
|
8124
8261
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8125
8262
|
}
|
|
8126
8263
|
function buildDeterministicIdempotencyKey(parts) {
|
|
8127
|
-
return (0,
|
|
8264
|
+
return (0, import_crypto4.createHash)("sha256").update(JSON.stringify(parts)).digest("hex");
|
|
8128
8265
|
}
|
|
8129
8266
|
function formatCliErrorDetail(err) {
|
|
8130
8267
|
if (err instanceof RemixError) {
|
|
@@ -8135,25 +8272,6 @@ function formatCliErrorDetail(err) {
|
|
|
8135
8272
|
}
|
|
8136
8273
|
return typeof err === "string" && err.trim() ? err.trim() : null;
|
|
8137
8274
|
}
|
|
8138
|
-
async function pollAppReady(api, appId) {
|
|
8139
|
-
const started = Date.now();
|
|
8140
|
-
let delay = 2e3;
|
|
8141
|
-
while (Date.now() - started < 20 * 60 * 1e3) {
|
|
8142
|
-
const appResp = await api.getApp(appId);
|
|
8143
|
-
const app = unwrapResponseObject(appResp, "app");
|
|
8144
|
-
const status = typeof app.status === "string" ? app.status : "";
|
|
8145
|
-
if (status === "ready") return app;
|
|
8146
|
-
if (status === "error") {
|
|
8147
|
-
throw new RemixError("App is in error state.", {
|
|
8148
|
-
exitCode: 1,
|
|
8149
|
-
hint: typeof app.statusError === "string" ? app.statusError : null
|
|
8150
|
-
});
|
|
8151
|
-
}
|
|
8152
|
-
await sleep(delay);
|
|
8153
|
-
delay = Math.min(1e4, Math.floor(delay * 1.4));
|
|
8154
|
-
}
|
|
8155
|
-
throw new RemixError("Timed out waiting for app to become ready.", { exitCode: 1 });
|
|
8156
|
-
}
|
|
8157
8275
|
async function pollChangeStep(api, appId, changeStepId) {
|
|
8158
8276
|
const started = Date.now();
|
|
8159
8277
|
let delay = 1500;
|
|
@@ -8428,638 +8546,567 @@ async function ensureActiveLaneBinding(params) {
|
|
|
8428
8546
|
hint: `Run \`remix collab init\` on branch ${resolved.currentBranch} before running ${params.operation ?? "this command"}.`
|
|
8429
8547
|
});
|
|
8430
8548
|
}
|
|
8431
|
-
|
|
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();
|
|
8432
8587
|
let repoRoot;
|
|
8433
8588
|
try {
|
|
8434
8589
|
repoRoot = await findGitRoot(params.cwd);
|
|
8435
8590
|
} catch (error) {
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
repoRoot: null,
|
|
8440
|
-
appId: null,
|
|
8441
|
-
currentBranch: null,
|
|
8442
|
-
branchName: null,
|
|
8443
|
-
headCommitHash: null,
|
|
8444
|
-
worktreeClean: false,
|
|
8445
|
-
syncStatus: null,
|
|
8446
|
-
syncTargetCommitHash: null,
|
|
8447
|
-
syncTargetCommitId: null,
|
|
8448
|
-
reconcileTargetHeadCommitHash: null,
|
|
8449
|
-
reconcileTargetHeadCommitId: null,
|
|
8450
|
-
warnings: [],
|
|
8451
|
-
hint: message
|
|
8452
|
-
};
|
|
8591
|
+
detected.status = "not_git_repo";
|
|
8592
|
+
detected.hint = formatCliErrorDetail(error) ?? "Not inside a git repository.";
|
|
8593
|
+
return detected;
|
|
8453
8594
|
}
|
|
8454
|
-
|
|
8595
|
+
detected.repoRoot = repoRoot;
|
|
8596
|
+
const bindingResolution = await resolveActiveLaneBinding({ repoRoot, api: params.api ?? void 0 });
|
|
8455
8597
|
if (bindingResolution.status === "not_bound") {
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
currentBranch: null,
|
|
8461
|
-
branchName: null,
|
|
8462
|
-
headCommitHash: null,
|
|
8463
|
-
worktreeClean: false,
|
|
8464
|
-
syncStatus: null,
|
|
8465
|
-
syncTargetCommitHash: null,
|
|
8466
|
-
syncTargetCommitId: null,
|
|
8467
|
-
reconcileTargetHeadCommitHash: null,
|
|
8468
|
-
reconcileTargetHeadCommitId: null,
|
|
8469
|
-
warnings: [],
|
|
8470
|
-
hint: "Run `remix collab init` first."
|
|
8471
|
-
};
|
|
8598
|
+
detected.status = "not_bound";
|
|
8599
|
+
detected.repoState = "binding_problem";
|
|
8600
|
+
detected.hint = "Run `remix collab init` first.";
|
|
8601
|
+
return detected;
|
|
8472
8602
|
}
|
|
8473
8603
|
if (bindingResolution.status === "missing_branch_binding") {
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
headCommitHash: null,
|
|
8481
|
-
worktreeClean: false,
|
|
8482
|
-
syncStatus: null,
|
|
8483
|
-
syncTargetCommitHash: null,
|
|
8484
|
-
syncTargetCommitId: null,
|
|
8485
|
-
reconcileTargetHeadCommitHash: null,
|
|
8486
|
-
reconcileTargetHeadCommitId: null,
|
|
8487
|
-
warnings: [],
|
|
8488
|
-
hint: `Current branch ${bindingResolution.currentBranch ?? "(detached)"} is not yet bound to a Remix lane.`
|
|
8489
|
-
};
|
|
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;
|
|
8490
8610
|
}
|
|
8491
8611
|
if (bindingResolution.status === "ambiguous_family_selection") {
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8498
|
-
headCommitHash: null,
|
|
8499
|
-
worktreeClean: false,
|
|
8500
|
-
syncStatus: null,
|
|
8501
|
-
syncTargetCommitHash: null,
|
|
8502
|
-
syncTargetCommitId: null,
|
|
8503
|
-
reconcileTargetHeadCommitHash: null,
|
|
8504
|
-
reconcileTargetHeadCommitId: null,
|
|
8505
|
-
warnings: [],
|
|
8506
|
-
hint: "Multiple canonical Remix families match this repository. Continue from a checkout already bound to the intended family, or run `remix collab init --force-new` to create a new canonical family."
|
|
8507
|
-
};
|
|
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;
|
|
8508
8618
|
}
|
|
8509
8619
|
if (bindingResolution.status === "binding_conflict") {
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
worktreeClean: false,
|
|
8518
|
-
syncStatus: null,
|
|
8519
|
-
syncTargetCommitHash: null,
|
|
8520
|
-
syncTargetCommitId: null,
|
|
8521
|
-
reconcileTargetHeadCommitHash: null,
|
|
8522
|
-
reconcileTargetHeadCommitId: null,
|
|
8523
|
-
warnings: [],
|
|
8524
|
-
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.`
|
|
8525
|
-
};
|
|
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;
|
|
8526
8627
|
}
|
|
8527
8628
|
const binding = bindingResolution.binding;
|
|
8528
|
-
|
|
8629
|
+
detected.binding = binding;
|
|
8630
|
+
const [currentBranch, localCommitHash, worktreeStatus] = await Promise.all([
|
|
8529
8631
|
getCurrentBranch(repoRoot),
|
|
8530
8632
|
getHeadCommitHash(repoRoot),
|
|
8531
8633
|
getWorktreeStatus(repoRoot)
|
|
8532
8634
|
]);
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
syncTargetCommitId: null,
|
|
8564
|
-
reconcileTargetHeadCommitHash: null,
|
|
8565
|
-
reconcileTargetHeadCommitId: null,
|
|
8566
|
-
warnings: [],
|
|
8567
|
-
hint: buildBranchMismatchHint({
|
|
8568
|
-
currentBranch,
|
|
8569
|
-
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
|
|
8570
8665
|
})
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
repoFingerprint: binding.repoFingerprint ?? void 0,
|
|
8576
|
-
remoteUrl: binding.remoteUrl ?? void 0,
|
|
8577
|
-
defaultBranch: binding.defaultBranch ?? void 0,
|
|
8578
|
-
dryRun: true
|
|
8579
|
-
});
|
|
8580
|
-
const sync = unwrapResponseObject(syncResp, "sync result");
|
|
8581
|
-
if (sync.status === "conflict_risk") {
|
|
8582
|
-
return {
|
|
8583
|
-
status: "metadata_conflict",
|
|
8584
|
-
repoRoot,
|
|
8585
|
-
appId: binding.currentAppId,
|
|
8586
|
-
currentBranch,
|
|
8587
|
-
branchName,
|
|
8588
|
-
headCommitHash,
|
|
8589
|
-
worktreeClean: worktreeStatus.isClean,
|
|
8590
|
-
syncStatus: sync.status,
|
|
8591
|
-
syncTargetCommitHash: sync.targetCommitHash,
|
|
8592
|
-
syncTargetCommitId: sync.targetCommitId,
|
|
8593
|
-
reconcileTargetHeadCommitHash: null,
|
|
8594
|
-
reconcileTargetHeadCommitId: null,
|
|
8595
|
-
warnings: sync.warnings,
|
|
8596
|
-
hint: sync.warnings.join("\n") || "Run the command from the correct bound repository."
|
|
8597
|
-
};
|
|
8598
|
-
}
|
|
8599
|
-
if (sync.status === "up_to_date" || sync.status === "ready_to_fast_forward") {
|
|
8600
|
-
return {
|
|
8601
|
-
status: sync.status,
|
|
8602
|
-
repoRoot,
|
|
8603
|
-
appId: binding.currentAppId,
|
|
8604
|
-
currentBranch,
|
|
8605
|
-
branchName,
|
|
8606
|
-
headCommitHash,
|
|
8607
|
-
worktreeClean: worktreeStatus.isClean,
|
|
8608
|
-
syncStatus: sync.status,
|
|
8609
|
-
syncTargetCommitHash: sync.targetCommitHash,
|
|
8610
|
-
syncTargetCommitId: sync.targetCommitId,
|
|
8611
|
-
reconcileTargetHeadCommitHash: null,
|
|
8612
|
-
reconcileTargetHeadCommitId: null,
|
|
8613
|
-
warnings: sync.warnings,
|
|
8614
|
-
hint: null
|
|
8615
|
-
};
|
|
8616
|
-
}
|
|
8617
|
-
const reconcileResp = await params.api.preflightAppReconcile(binding.currentAppId, {
|
|
8618
|
-
localHeadCommitHash: headCommitHash,
|
|
8619
|
-
repoFingerprint: binding.repoFingerprint ?? void 0,
|
|
8620
|
-
remoteUrl: binding.remoteUrl ?? void 0,
|
|
8621
|
-
defaultBranch: binding.defaultBranch ?? void 0
|
|
8622
|
-
});
|
|
8623
|
-
const reconcile = unwrapResponseObject(reconcileResp, "reconcile preflight");
|
|
8624
|
-
if (reconcile.status === "metadata_conflict") {
|
|
8625
|
-
return {
|
|
8626
|
-
status: "metadata_conflict",
|
|
8627
|
-
repoRoot,
|
|
8628
|
-
appId: binding.currentAppId,
|
|
8629
|
-
currentBranch,
|
|
8630
|
-
branchName,
|
|
8631
|
-
headCommitHash,
|
|
8632
|
-
worktreeClean: worktreeStatus.isClean,
|
|
8633
|
-
syncStatus: sync.status,
|
|
8634
|
-
syncTargetCommitHash: sync.targetCommitHash,
|
|
8635
|
-
syncTargetCommitId: sync.targetCommitId,
|
|
8636
|
-
reconcileTargetHeadCommitHash: reconcile.targetHeadCommitHash,
|
|
8637
|
-
reconcileTargetHeadCommitId: reconcile.targetHeadCommitId,
|
|
8638
|
-
warnings: reconcile.warnings,
|
|
8639
|
-
hint: reconcile.warnings.join("\n") || "Run the command from the correct bound repository."
|
|
8640
|
-
};
|
|
8641
|
-
}
|
|
8642
|
-
if (reconcile.status === "up_to_date") {
|
|
8643
|
-
return {
|
|
8644
|
-
status: "up_to_date",
|
|
8645
|
-
repoRoot,
|
|
8646
|
-
appId: binding.currentAppId,
|
|
8647
|
-
currentBranch,
|
|
8648
|
-
branchName,
|
|
8649
|
-
headCommitHash,
|
|
8650
|
-
worktreeClean: worktreeStatus.isClean,
|
|
8651
|
-
syncStatus: sync.status,
|
|
8652
|
-
syncTargetCommitHash: sync.targetCommitHash,
|
|
8653
|
-
syncTargetCommitId: sync.targetCommitId,
|
|
8654
|
-
reconcileTargetHeadCommitHash: reconcile.targetHeadCommitHash,
|
|
8655
|
-
reconcileTargetHeadCommitId: reconcile.targetHeadCommitId,
|
|
8656
|
-
warnings: reconcile.warnings,
|
|
8657
|
-
hint: null
|
|
8658
|
-
};
|
|
8666
|
+
]);
|
|
8667
|
+
detected.currentSnapshotHash = inspection.snapshotHash;
|
|
8668
|
+
detected.pendingFinalize = pendingFinalize;
|
|
8669
|
+
return detected;
|
|
8659
8670
|
}
|
|
8660
|
-
return {
|
|
8661
|
-
status: "reconcile_required",
|
|
8662
|
-
repoRoot,
|
|
8663
|
-
appId: binding.currentAppId,
|
|
8664
|
-
currentBranch,
|
|
8665
|
-
branchName,
|
|
8666
|
-
headCommitHash,
|
|
8667
|
-
worktreeClean: worktreeStatus.isClean,
|
|
8668
|
-
syncStatus: sync.status,
|
|
8669
|
-
syncTargetCommitHash: sync.targetCommitHash,
|
|
8670
|
-
syncTargetCommitId: sync.targetCommitId,
|
|
8671
|
-
reconcileTargetHeadCommitHash: reconcile.targetHeadCommitHash,
|
|
8672
|
-
reconcileTargetHeadCommitId: reconcile.targetHeadCommitId,
|
|
8673
|
-
warnings: reconcile.warnings,
|
|
8674
|
-
hint: reconcile.warnings.join("\n") || "Run `remix collab reconcile` first because the local history is no longer fast-forward compatible with the app."
|
|
8675
|
-
};
|
|
8676
|
-
}
|
|
8677
|
-
var DEFAULT_ACQUIRE_TIMEOUT_MS = 15e3;
|
|
8678
|
-
var DEFAULT_STALE_MS = 45e3;
|
|
8679
|
-
var DEFAULT_HEARTBEAT_MS = 5e3;
|
|
8680
|
-
var RETRY_DELAY_MS = 250;
|
|
8681
|
-
var heldLocks = /* @__PURE__ */ new Map();
|
|
8682
|
-
function sleep2(ms) {
|
|
8683
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8684
|
-
}
|
|
8685
|
-
function createOwner(params) {
|
|
8686
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8687
|
-
return {
|
|
8688
|
-
operation: params.operation,
|
|
8689
|
-
repoRoot: params.repoRoot,
|
|
8690
|
-
pid: process.pid,
|
|
8691
|
-
hostname: import_os2.default.hostname(),
|
|
8692
|
-
startedAt: now,
|
|
8693
|
-
heartbeatAt: now,
|
|
8694
|
-
version: process.version,
|
|
8695
|
-
requestId: params.requestId?.trim() || null
|
|
8696
|
-
};
|
|
8697
|
-
}
|
|
8698
|
-
async function writeOwnerMetadata(ownerPath, owner) {
|
|
8699
|
-
await import_promises16.default.writeFile(ownerPath, `${JSON.stringify(owner, null, 2)}
|
|
8700
|
-
`, "utf8");
|
|
8701
|
-
}
|
|
8702
|
-
async function readOwnerMetadata(ownerPath) {
|
|
8703
8671
|
try {
|
|
8704
|
-
const
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
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
|
|
8719
8703
|
};
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
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;
|
|
8730
8776
|
} catch (error) {
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
return
|
|
8777
|
+
detected.status = "remote_error";
|
|
8778
|
+
detected.hint = formatCliErrorDetail(error) ?? "Failed to detect the current Remix repo state.";
|
|
8779
|
+
return detected;
|
|
8734
8780
|
}
|
|
8735
8781
|
}
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
const
|
|
8740
|
-
|
|
8741
|
-
const stat = await import_promises16.default.stat(ownerPath).catch(() => null);
|
|
8742
|
-
if (stat) return stat.mtimeMs;
|
|
8743
|
-
const dirStat = await import_promises16.default.stat(lockDir).catch(() => null);
|
|
8744
|
-
if (dirStat) return dirStat.mtimeMs;
|
|
8745
|
-
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;
|
|
8746
8787
|
}
|
|
8747
|
-
|
|
8748
|
-
|
|
8788
|
+
function readMetadataActor(job) {
|
|
8789
|
+
const actor = job.metadata.actor;
|
|
8790
|
+
return actor && typeof actor === "object" ? actor : void 0;
|
|
8749
8791
|
}
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
try {
|
|
8755
|
-
await writeOwnerMetadata(ownerPath, owner);
|
|
8756
|
-
} catch (error) {
|
|
8757
|
-
await import_promises16.default.rm(lockDir, { recursive: true, force: true }).catch(() => void 0);
|
|
8758
|
-
throw error;
|
|
8759
|
-
}
|
|
8760
|
-
return true;
|
|
8761
|
-
} catch (error) {
|
|
8762
|
-
if (error?.code === "EEXIST") return false;
|
|
8763
|
-
throw error;
|
|
8764
|
-
}
|
|
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();
|
|
8765
8796
|
}
|
|
8766
|
-
function
|
|
8767
|
-
const
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
`Waited ${params.waitedMs}ms for the repo mutation lock.`,
|
|
8775
|
-
`Stale lock threshold: ${params.staleMs}ms.`,
|
|
8776
|
-
"Retry after the active operation finishes. If the process crashed, wait for stale lock recovery or remove the stale lock manually if necessary."
|
|
8777
|
-
];
|
|
8778
|
-
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;
|
|
8779
8805
|
}
|
|
8780
|
-
function
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
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
|
+
};
|
|
8785
8813
|
}
|
|
8786
|
-
function
|
|
8814
|
+
function buildWorkspaceMetadata(params) {
|
|
8787
8815
|
return {
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
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
|
|
8791
8828
|
};
|
|
8792
8829
|
}
|
|
8793
|
-
async function
|
|
8794
|
-
const
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
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
|
+
});
|
|
8808
8849
|
}
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
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 ?? "") }
|
|
8869
9029
|
});
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
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
|
+
}
|
|
8880
9051
|
});
|
|
9052
|
+
throw error;
|
|
8881
9053
|
} finally {
|
|
8882
|
-
await
|
|
9054
|
+
await params.release();
|
|
8883
9055
|
}
|
|
8884
9056
|
}
|
|
8885
|
-
async function
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
repoRoot,
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
});
|
|
8906
|
-
const headCommitHash = await getHeadCommitHash(repoRoot);
|
|
8907
|
-
const repoSnapshot = await captureRepoSnapshot(repoRoot);
|
|
8908
|
-
if (!headCommitHash) {
|
|
8909
|
-
throw new RemixError("Failed to resolve local HEAD commit.", { exitCode: 1 });
|
|
8910
|
-
}
|
|
8911
|
-
const resp = await params.api.syncLocalApp(binding.currentAppId, {
|
|
8912
|
-
baseCommitHash: headCommitHash,
|
|
8913
|
-
repoFingerprint: binding.repoFingerprint ?? void 0,
|
|
8914
|
-
remoteUrl: binding.remoteUrl ?? void 0,
|
|
8915
|
-
defaultBranch: binding.defaultBranch ?? void 0,
|
|
8916
|
-
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 ?? {}
|
|
8917
9077
|
});
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
if (sync.status === "up_to_date") {
|
|
8932
|
-
return {
|
|
8933
|
-
status: sync.status,
|
|
8934
|
-
branch,
|
|
8935
|
-
repoRoot,
|
|
8936
|
-
baseCommitHash: sync.baseCommitHash,
|
|
8937
|
-
targetCommitHash: sync.targetCommitHash,
|
|
8938
|
-
targetCommitId: sync.targetCommitId,
|
|
8939
|
-
stats: sync.stats,
|
|
8940
|
-
localCommitHash: headCommitHash,
|
|
8941
|
-
applied: false,
|
|
8942
|
-
dryRun: params.dryRun
|
|
8943
|
-
};
|
|
8944
|
-
}
|
|
8945
|
-
const previewResult = {
|
|
8946
|
-
status: sync.status,
|
|
8947
|
-
branch,
|
|
8948
|
-
repoRoot,
|
|
8949
|
-
baseCommitHash: sync.baseCommitHash,
|
|
8950
|
-
targetCommitHash: sync.targetCommitHash,
|
|
8951
|
-
targetCommitId: sync.targetCommitId,
|
|
8952
|
-
stats: sync.stats,
|
|
8953
|
-
bundleRef: sync.bundleRef,
|
|
8954
|
-
bundleSizeBytes: sync.bundleSizeBytes,
|
|
8955
|
-
localCommitHash: headCommitHash,
|
|
8956
|
-
applied: false,
|
|
8957
|
-
dryRun: params.dryRun
|
|
8958
|
-
};
|
|
8959
|
-
if (params.dryRun) {
|
|
8960
|
-
return previewResult;
|
|
8961
|
-
}
|
|
8962
|
-
if (!sync.bundleBase64 || !sync.bundleRef) {
|
|
8963
|
-
throw new RemixError("Sync bundle payload is missing.", { exitCode: 1 });
|
|
8964
|
-
}
|
|
8965
|
-
const bundleBase64 = sync.bundleBase64;
|
|
8966
|
-
const bundleRef = sync.bundleRef;
|
|
8967
|
-
return withRepoMutationLock(
|
|
8968
|
-
{
|
|
8969
|
-
cwd: repoRoot,
|
|
8970
|
-
operation: "collabSync"
|
|
8971
|
-
},
|
|
8972
|
-
async ({ repoRoot: lockedRepoRoot, warnings }) => {
|
|
8973
|
-
await assertRepoSnapshotUnchanged(lockedRepoRoot, repoSnapshot, {
|
|
8974
|
-
operation: "`remix collab sync`",
|
|
8975
|
-
recoveryHint: "The repository changed after sync was prepared. Review the local changes and rerun `remix collab sync`."
|
|
8976
|
-
});
|
|
8977
|
-
await ensureCleanWorktree(lockedRepoRoot);
|
|
8978
|
-
const lockedBranch = await requireCurrentBranch(lockedRepoRoot);
|
|
8979
|
-
assertBoundBranchMatch({
|
|
8980
|
-
currentBranch: lockedBranch,
|
|
8981
|
-
branchName: binding.branchName,
|
|
8982
|
-
allowBranchMismatch: params.allowBranchMismatch,
|
|
8983
|
-
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
|
|
8984
9091
|
});
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
await import_promises17.default.writeFile(bundlePath, Buffer.from(bundleBase64, "base64"));
|
|
8989
|
-
await importGitBundle(lockedRepoRoot, bundlePath, bundleRef);
|
|
8990
|
-
await ensureCommitExists(lockedRepoRoot, sync.targetCommitHash);
|
|
8991
|
-
const localCommitHash = await fastForwardToCommit(lockedRepoRoot, sync.targetCommitHash);
|
|
8992
|
-
return {
|
|
8993
|
-
...previewResult,
|
|
8994
|
-
localCommitHash,
|
|
8995
|
-
applied: true,
|
|
8996
|
-
dryRun: false,
|
|
8997
|
-
...warnings.length > 0 ? { warnings } : {}
|
|
8998
|
-
};
|
|
8999
|
-
} finally {
|
|
9000
|
-
await import_promises17.default.rm(tempDir, { recursive: true, force: true });
|
|
9001
|
-
}
|
|
9092
|
+
results.push(result);
|
|
9093
|
+
await removePendingFinalizeJob(job.id);
|
|
9094
|
+
} catch {
|
|
9002
9095
|
}
|
|
9003
|
-
);
|
|
9004
|
-
}
|
|
9005
|
-
function assertSupportedRecordingPreflight(preflight) {
|
|
9006
|
-
if (preflight.status === "not_bound") {
|
|
9007
|
-
throw new RemixError("Repository is not bound to Remix.", {
|
|
9008
|
-
exitCode: 2,
|
|
9009
|
-
hint: preflight.hint
|
|
9010
|
-
});
|
|
9011
|
-
}
|
|
9012
|
-
if (preflight.status === "branch_binding_missing") {
|
|
9013
|
-
throw new RemixError("Current branch is not yet bound to a Remix lane.", {
|
|
9014
|
-
exitCode: 2,
|
|
9015
|
-
hint: preflight.hint
|
|
9016
|
-
});
|
|
9017
|
-
}
|
|
9018
|
-
if (preflight.status === "family_ambiguous") {
|
|
9019
|
-
throw new RemixError("Multiple canonical Remix families match this repository.", {
|
|
9020
|
-
exitCode: 2,
|
|
9021
|
-
hint: preflight.hint
|
|
9022
|
-
});
|
|
9023
|
-
}
|
|
9024
|
-
if (preflight.status === "not_git_repo") {
|
|
9025
|
-
throw new RemixError(preflight.hint || "Not inside a git repository.", {
|
|
9026
|
-
exitCode: 2,
|
|
9027
|
-
hint: preflight.hint
|
|
9028
|
-
});
|
|
9029
|
-
}
|
|
9030
|
-
if (preflight.status === "missing_head") {
|
|
9031
|
-
throw new RemixError("Failed to resolve local HEAD commit.", {
|
|
9032
|
-
exitCode: 1,
|
|
9033
|
-
hint: preflight.hint
|
|
9034
|
-
});
|
|
9035
|
-
}
|
|
9036
|
-
if (preflight.status === "branch_mismatch") {
|
|
9037
|
-
assertBoundBranchMatch({
|
|
9038
|
-
currentBranch: preflight.currentBranch,
|
|
9039
|
-
branchName: preflight.branchName,
|
|
9040
|
-
allowBranchMismatch: false,
|
|
9041
|
-
operation: "`remix collab add`"
|
|
9042
|
-
});
|
|
9043
|
-
}
|
|
9044
|
-
if (preflight.status === "metadata_conflict") {
|
|
9045
|
-
throw new RemixError("Local repository metadata conflicts with the bound Remix app.", {
|
|
9046
|
-
exitCode: 2,
|
|
9047
|
-
hint: preflight.hint
|
|
9048
|
-
});
|
|
9049
|
-
}
|
|
9050
|
-
if (preflight.status === "reconcile_required") {
|
|
9051
|
-
throw new RemixError("Local repository cannot be fast-forward synced.", {
|
|
9052
|
-
exitCode: 2,
|
|
9053
|
-
hint: preflight.hint
|
|
9054
|
-
});
|
|
9055
9096
|
}
|
|
9097
|
+
return results;
|
|
9056
9098
|
}
|
|
9057
|
-
|
|
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) {
|
|
9058
9105
|
const repoRoot = await findGitRoot(params.cwd);
|
|
9059
9106
|
const binding = await ensureActiveLaneBinding({
|
|
9060
9107
|
repoRoot,
|
|
9061
9108
|
api: params.api,
|
|
9062
|
-
operation: "`remix collab
|
|
9109
|
+
operation: "`remix collab finalize-turn`"
|
|
9063
9110
|
});
|
|
9064
9111
|
if (!binding) {
|
|
9065
9112
|
throw new RemixError("Repository is not bound to Remix.", {
|
|
@@ -9068,385 +9115,114 @@ async function collabAdd(params) {
|
|
|
9068
9115
|
});
|
|
9069
9116
|
}
|
|
9070
9117
|
const prompt = params.prompt.trim();
|
|
9118
|
+
const assistantResponse = params.assistantResponse.trim();
|
|
9071
9119
|
if (!prompt) throw new RemixError("Prompt is required.", { exitCode: 2 });
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
const autoSyncEnabled = params.sync !== false;
|
|
9076
|
-
const run = async (lockWarnings = []) => {
|
|
9077
|
-
const preflight = await collabRecordingPreflight({
|
|
9078
|
-
api: params.api,
|
|
9079
|
-
cwd: repoRoot,
|
|
9080
|
-
allowBranchMismatch: params.allowBranchMismatch
|
|
9081
|
-
});
|
|
9082
|
-
assertSupportedRecordingPreflight(preflight);
|
|
9083
|
-
const branch = preflight.currentBranch;
|
|
9084
|
-
assertBoundBranchMatch({
|
|
9085
|
-
currentBranch: branch,
|
|
9086
|
-
branchName: binding.branchName,
|
|
9087
|
-
allowBranchMismatch: params.allowBranchMismatch,
|
|
9088
|
-
operation: "`remix collab add`"
|
|
9089
|
-
});
|
|
9090
|
-
let headCommitHash = preflight.headCommitHash;
|
|
9091
|
-
if (!headCommitHash) {
|
|
9092
|
-
throw new RemixError("Failed to resolve local HEAD commit.", { exitCode: 1 });
|
|
9093
|
-
}
|
|
9094
|
-
const worktreeStatus = await getWorktreeStatus(repoRoot);
|
|
9095
|
-
if (preflight.status === "ready_to_fast_forward") {
|
|
9096
|
-
if (!autoSyncEnabled) {
|
|
9097
|
-
throw new RemixError("Local repository is stale and `collab add` sync automation is disabled.", {
|
|
9098
|
-
exitCode: 2,
|
|
9099
|
-
hint: "Run `remix collab sync` first, or rerun without disabling sync automation."
|
|
9100
|
-
});
|
|
9101
|
-
}
|
|
9102
|
-
if (!worktreeStatus.isClean && diffSource !== "worktree") {
|
|
9103
|
-
throw new RemixError("Automatic stale-work replay requires the current worktree diff.", {
|
|
9104
|
-
exitCode: 2,
|
|
9105
|
-
hint: "Use `remix collab add` without an external diff while the local repo is dirty, or clean the repo before submitting an external diff."
|
|
9106
|
-
});
|
|
9107
|
-
}
|
|
9108
|
-
if (worktreeStatus.isClean) {
|
|
9109
|
-
await collabSync({
|
|
9110
|
-
api: params.api,
|
|
9111
|
-
cwd: repoRoot,
|
|
9112
|
-
dryRun: false,
|
|
9113
|
-
allowBranchMismatch: params.allowBranchMismatch
|
|
9114
|
-
});
|
|
9115
|
-
headCommitHash = await getHeadCommitHash(repoRoot);
|
|
9116
|
-
if (!headCommitHash) {
|
|
9117
|
-
throw new RemixError("Failed to resolve local HEAD after syncing.", { exitCode: 1 });
|
|
9118
|
-
}
|
|
9119
|
-
} else {
|
|
9120
|
-
const staleWorkSnapshot = await captureRepoSnapshot(repoRoot, { includeWorkspaceDiffHash: true });
|
|
9121
|
-
const preserved = await preserveWorkspaceChanges(repoRoot, "remix-add-preserve");
|
|
9122
|
-
try {
|
|
9123
|
-
await assertRepoSnapshotUnchanged(repoRoot, staleWorkSnapshot, {
|
|
9124
|
-
operation: "`remix collab add` stale-work pre-sync",
|
|
9125
|
-
recoveryHint: "The worktree changed while local changes were being preserved. Review the local changes and rerun `remix collab add`."
|
|
9126
|
-
});
|
|
9127
|
-
await discardTrackedChanges(repoRoot, "`remix collab add`");
|
|
9128
|
-
await discardCapturedUntrackedChanges(repoRoot, preserved.includedUntrackedPaths);
|
|
9129
|
-
await collabSync({
|
|
9130
|
-
api: params.api,
|
|
9131
|
-
cwd: repoRoot,
|
|
9132
|
-
dryRun: false,
|
|
9133
|
-
allowBranchMismatch: params.allowBranchMismatch
|
|
9134
|
-
});
|
|
9135
|
-
} catch (err) {
|
|
9136
|
-
const detail = formatCliErrorDetail(err);
|
|
9137
|
-
const hint = [
|
|
9138
|
-
detail,
|
|
9139
|
-
`The preserved local diff is available at: ${preserved.preservedDiffPath}`
|
|
9140
|
-
].filter(Boolean).join("\n\n");
|
|
9141
|
-
throw new RemixError("Failed to sync the stale repository before submitting the change step.", {
|
|
9142
|
-
exitCode: err instanceof RemixError ? err.exitCode : 1,
|
|
9143
|
-
hint
|
|
9144
|
-
});
|
|
9145
|
-
}
|
|
9146
|
-
headCommitHash = await getHeadCommitHash(repoRoot);
|
|
9147
|
-
if (!headCommitHash) {
|
|
9148
|
-
throw new RemixError("Failed to resolve local HEAD after syncing.", { exitCode: 1 });
|
|
9149
|
-
}
|
|
9150
|
-
const deterministicReapply = await reapplyPreservedWorkspaceChanges(repoRoot, preserved);
|
|
9151
|
-
if (deterministicReapply.status === "failed") {
|
|
9152
|
-
const hint = [
|
|
9153
|
-
deterministicReapply.detail,
|
|
9154
|
-
`The preserved local diff is available at: ${preserved.preservedDiffPath}`
|
|
9155
|
-
].filter(Boolean).join("\n\n");
|
|
9156
|
-
throw new RemixError("Failed to restore preserved local changes after syncing.", {
|
|
9157
|
-
exitCode: 1,
|
|
9158
|
-
hint
|
|
9159
|
-
});
|
|
9160
|
-
}
|
|
9161
|
-
if (deterministicReapply.status === "conflict") {
|
|
9162
|
-
try {
|
|
9163
|
-
const replayResp = await params.api.startChangeStepReplay(binding.currentAppId, {
|
|
9164
|
-
prompt,
|
|
9165
|
-
assistantResponse: assistantResponse ?? void 0,
|
|
9166
|
-
diff: await import_promises15.default.readFile(preserved.preservedDiffPath, "utf8"),
|
|
9167
|
-
baseCommitHash: preserved.baseHeadCommitHash,
|
|
9168
|
-
targetHeadCommitHash: headCommitHash,
|
|
9169
|
-
expectedPaths: preserved.stagePlan.expectedPaths,
|
|
9170
|
-
actor: params.actor,
|
|
9171
|
-
workspaceMetadata: {
|
|
9172
|
-
branch,
|
|
9173
|
-
repoRoot,
|
|
9174
|
-
remoteUrl: binding.remoteUrl,
|
|
9175
|
-
defaultBranch: binding.defaultBranch
|
|
9176
|
-
},
|
|
9177
|
-
idempotencyKey: buildDeterministicIdempotencyKey({
|
|
9178
|
-
appId: binding.currentAppId,
|
|
9179
|
-
baseCommitHash: preserved.baseHeadCommitHash,
|
|
9180
|
-
targetHeadCommitHash: headCommitHash,
|
|
9181
|
-
prompt,
|
|
9182
|
-
assistantResponse,
|
|
9183
|
-
preservedDiffSha256: preserved.preservedDiffSha256
|
|
9184
|
-
})
|
|
9185
|
-
});
|
|
9186
|
-
const startedReplay = unwrapResponseObject(replayResp, "change step replay");
|
|
9187
|
-
const replay = await pollChangeStepReplay(params.api, binding.currentAppId, String(startedReplay.id));
|
|
9188
|
-
const replayDiffResp = await params.api.getChangeStepReplayDiff(binding.currentAppId, String(replay.id));
|
|
9189
|
-
const replayDiff = unwrapResponseObject(replayDiffResp, "change step replay diff");
|
|
9190
|
-
const { backupPath: backupPath2, diffSha256 } = await writeTempUnifiedDiffBackup(replayDiff.diff, "remix-add-ai-replay");
|
|
9191
|
-
const replayApply = await reapplyPreservedWorkspaceChanges(repoRoot, {
|
|
9192
|
-
baseHeadCommitHash: headCommitHash,
|
|
9193
|
-
preservedDiffPath: backupPath2,
|
|
9194
|
-
preservedDiffSha256: diffSha256,
|
|
9195
|
-
includedUntrackedPaths: [],
|
|
9196
|
-
stagePlan: preserved.stagePlan
|
|
9197
|
-
});
|
|
9198
|
-
if (replayApply.status !== "clean") {
|
|
9199
|
-
const hint = [
|
|
9200
|
-
replayApply.detail,
|
|
9201
|
-
`The preserved local diff is available at: ${preserved.preservedDiffPath}`,
|
|
9202
|
-
`The AI-replayed diff is available at: ${backupPath2}`
|
|
9203
|
-
].filter(Boolean).join("\n\n");
|
|
9204
|
-
throw new RemixError("AI-assisted stale-work replay produced a diff that could not be applied locally.", {
|
|
9205
|
-
exitCode: 1,
|
|
9206
|
-
hint
|
|
9207
|
-
});
|
|
9208
|
-
}
|
|
9209
|
-
} catch (err) {
|
|
9210
|
-
const detail = formatCliErrorDetail(err);
|
|
9211
|
-
const hint = [
|
|
9212
|
-
detail,
|
|
9213
|
-
`The preserved local diff is available at: ${preserved.preservedDiffPath}`,
|
|
9214
|
-
"Resolve the local conflict manually if needed, then rerun `remix collab add`."
|
|
9215
|
-
].filter(Boolean).join("\n\n");
|
|
9216
|
-
throw new RemixError("AI-assisted stale-work replay could not complete safely.", {
|
|
9217
|
-
exitCode: err instanceof RemixError ? err.exitCode : 1,
|
|
9218
|
-
hint
|
|
9219
|
-
});
|
|
9220
|
-
}
|
|
9221
|
-
}
|
|
9222
|
-
}
|
|
9223
|
-
}
|
|
9224
|
-
const workspaceSnapshot = diffSource === "external" ? null : await getWorkspaceSnapshot(repoRoot);
|
|
9225
|
-
const submissionSnapshot = diffSource === "worktree" ? await captureRepoSnapshot(repoRoot, { includeWorkspaceDiffHash: true }) : null;
|
|
9226
|
-
const diff = params.diff ?? workspaceSnapshot?.diff ?? "";
|
|
9227
|
-
if (!diff.trim()) {
|
|
9228
|
-
throw new RemixError("Diff is empty.", {
|
|
9229
|
-
exitCode: 2,
|
|
9230
|
-
hint: "Make changes first, or pass `--diff-file`/`--diff-stdin`."
|
|
9231
|
-
});
|
|
9232
|
-
}
|
|
9233
|
-
if (diffSource === "external") {
|
|
9234
|
-
const validation = await validateUnifiedDiff(repoRoot, diff);
|
|
9235
|
-
if (!validation.ok) {
|
|
9236
|
-
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.";
|
|
9237
|
-
const hint = [validation.detail, actionHint].filter(Boolean).join("\n\n");
|
|
9238
|
-
throw new RemixError("External diff validation failed.", {
|
|
9239
|
-
exitCode: validation.kind === "malformed_patch" ? 2 : 1,
|
|
9240
|
-
hint
|
|
9241
|
-
});
|
|
9242
|
-
}
|
|
9243
|
-
}
|
|
9244
|
-
headCommitHash = await getHeadCommitHash(repoRoot);
|
|
9245
|
-
if (!headCommitHash) {
|
|
9246
|
-
throw new RemixError("Failed to resolve local HEAD before creating the change step.", { exitCode: 1 });
|
|
9247
|
-
}
|
|
9248
|
-
const stats = summarizeUnifiedDiff(diff);
|
|
9249
|
-
const idempotencyKey = params.idempotencyKey?.trim() || buildDeterministicIdempotencyKey({
|
|
9250
|
-
appId: binding.currentAppId,
|
|
9251
|
-
upstreamAppId: binding.upstreamAppId,
|
|
9252
|
-
headCommitHash,
|
|
9253
|
-
prompt,
|
|
9254
|
-
assistantResponse,
|
|
9255
|
-
diff
|
|
9256
|
-
});
|
|
9257
|
-
const resp = await params.api.createChangeStep(binding.currentAppId, {
|
|
9258
|
-
threadId: binding.threadId ?? void 0,
|
|
9259
|
-
collabLaneId: binding.laneId ?? void 0,
|
|
9260
|
-
prompt,
|
|
9261
|
-
assistantResponse: assistantResponse ?? void 0,
|
|
9262
|
-
diff,
|
|
9263
|
-
baseCommitHash: headCommitHash,
|
|
9264
|
-
headCommitHash,
|
|
9265
|
-
changedFilesCount: stats.changedFilesCount,
|
|
9266
|
-
insertions: stats.insertions,
|
|
9267
|
-
deletions: stats.deletions,
|
|
9268
|
-
actor: params.actor,
|
|
9269
|
-
workspaceMetadata: {
|
|
9270
|
-
branch,
|
|
9271
|
-
repoRoot,
|
|
9272
|
-
remoteUrl: binding.remoteUrl,
|
|
9273
|
-
defaultBranch: binding.defaultBranch
|
|
9274
|
-
},
|
|
9275
|
-
idempotencyKey
|
|
9276
|
-
});
|
|
9277
|
-
const created = unwrapResponseObject(resp, "change step");
|
|
9278
|
-
const step = await pollChangeStep(params.api, binding.currentAppId, String(created.id));
|
|
9279
|
-
const canAutoSyncLocally = autoSyncEnabled && diffSource === "worktree";
|
|
9280
|
-
if (!autoSyncEnabled || !canAutoSyncLocally) {
|
|
9281
|
-
return attachWarnings(step, lockWarnings);
|
|
9282
|
-
}
|
|
9283
|
-
const { backupPath } = await writeTempUnifiedDiffBackup(diff, "remix-add");
|
|
9284
|
-
try {
|
|
9285
|
-
await pollAppReady(params.api, binding.currentAppId);
|
|
9286
|
-
if (submissionSnapshot) {
|
|
9287
|
-
await assertRepoSnapshotUnchanged(repoRoot, submissionSnapshot, {
|
|
9288
|
-
operation: "`remix collab add` auto-sync",
|
|
9289
|
-
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."
|
|
9290
|
-
});
|
|
9291
|
-
}
|
|
9292
|
-
await discardTrackedChanges(repoRoot, "`remix collab add`");
|
|
9293
|
-
await discardCapturedUntrackedChanges(repoRoot, workspaceSnapshot?.includedUntrackedPaths ?? []);
|
|
9294
|
-
await collabSync({
|
|
9295
|
-
api: params.api,
|
|
9296
|
-
cwd: repoRoot,
|
|
9297
|
-
dryRun: false,
|
|
9298
|
-
allowBranchMismatch: params.allowBranchMismatch
|
|
9299
|
-
});
|
|
9300
|
-
await import_promises15.default.rm(import_path4.default.dirname(backupPath), { recursive: true, force: true }).catch(() => void 0);
|
|
9301
|
-
} catch (err) {
|
|
9302
|
-
const detail = formatCliErrorDetail(err);
|
|
9303
|
-
const hint = [
|
|
9304
|
-
detail,
|
|
9305
|
-
`The submitted diff backup was preserved at: ${backupPath}`,
|
|
9306
|
-
"The change step already succeeded remotely. Inspect or reapply that diff manually if needed, then run `remix collab sync`."
|
|
9307
|
-
].filter(Boolean).join("\n\n");
|
|
9308
|
-
throw new RemixError("Change step succeeded remotely, but automatic local sync failed.", {
|
|
9309
|
-
exitCode: err instanceof RemixError ? err.exitCode : 1,
|
|
9310
|
-
hint
|
|
9311
|
-
});
|
|
9312
|
-
}
|
|
9313
|
-
return attachWarnings(step, lockWarnings);
|
|
9314
|
-
};
|
|
9315
|
-
if (diffSource === "worktree") {
|
|
9316
|
-
return withRepoMutationLock(
|
|
9317
|
-
{
|
|
9318
|
-
cwd: repoRoot,
|
|
9319
|
-
operation: "collabAdd"
|
|
9320
|
-
},
|
|
9321
|
-
async ({ warnings }) => run(warnings)
|
|
9322
|
-
);
|
|
9323
|
-
}
|
|
9324
|
-
return run();
|
|
9325
|
-
}
|
|
9326
|
-
function assertSupportedRecordingPreflight2(preflight) {
|
|
9327
|
-
if (preflight.status === "not_bound") {
|
|
9328
|
-
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`.", {
|
|
9329
9123
|
exitCode: 2,
|
|
9330
|
-
hint:
|
|
9124
|
+
hint: "Finalize turns now capture the real workspace boundary from the local snapshot store."
|
|
9331
9125
|
});
|
|
9332
9126
|
}
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
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 });
|
|
9338
9134
|
}
|
|
9339
|
-
if (
|
|
9340
|
-
throw new RemixError("
|
|
9341
|
-
exitCode: 2,
|
|
9342
|
-
hint: preflight.hint
|
|
9343
|
-
});
|
|
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 });
|
|
9344
9137
|
}
|
|
9345
|
-
if (
|
|
9346
|
-
throw new RemixError(
|
|
9138
|
+
if (detected.status === "metadata_conflict" || detected.status === "branch_mismatch") {
|
|
9139
|
+
throw new RemixError("Repository must be realigned before finalizing the turn.", {
|
|
9347
9140
|
exitCode: 2,
|
|
9348
|
-
hint:
|
|
9141
|
+
hint: detected.hint
|
|
9349
9142
|
});
|
|
9350
9143
|
}
|
|
9351
|
-
if (
|
|
9352
|
-
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.", {
|
|
9353
9146
|
exitCode: 1,
|
|
9354
|
-
hint:
|
|
9355
|
-
});
|
|
9356
|
-
}
|
|
9357
|
-
if (preflight.status === "branch_mismatch") {
|
|
9358
|
-
assertBoundBranchMatch({
|
|
9359
|
-
currentBranch: preflight.currentBranch,
|
|
9360
|
-
branchName: preflight.branchName,
|
|
9361
|
-
allowBranchMismatch: false,
|
|
9362
|
-
operation: "`remix collab record-turn`"
|
|
9147
|
+
hint: detected.hint
|
|
9363
9148
|
});
|
|
9364
9149
|
}
|
|
9365
|
-
if (
|
|
9366
|
-
throw new RemixError("
|
|
9150
|
+
if (detected.repoState === "server_only_changed") {
|
|
9151
|
+
throw new RemixError("Server changes must be pulled locally before finalizing this turn.", {
|
|
9367
9152
|
exitCode: 2,
|
|
9368
|
-
hint:
|
|
9153
|
+
hint: detected.hint
|
|
9369
9154
|
});
|
|
9370
9155
|
}
|
|
9371
|
-
if (
|
|
9372
|
-
throw new RemixError("
|
|
9156
|
+
if (detected.repoState === "external_local_base_changed") {
|
|
9157
|
+
throw new RemixError("The local checkout must be re-anchored before finalizing this turn.", {
|
|
9373
9158
|
exitCode: 2,
|
|
9374
|
-
hint:
|
|
9159
|
+
hint: detected.hint
|
|
9375
9160
|
});
|
|
9376
9161
|
}
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
repoRoot,
|
|
9382
|
-
api: params.api,
|
|
9383
|
-
operation: "`remix collab record-turn`"
|
|
9384
|
-
});
|
|
9385
|
-
if (!binding) {
|
|
9386
|
-
throw new RemixError("Repository is not bound to Remix.", {
|
|
9387
|
-
exitCode: 2,
|
|
9388
|
-
hint: "Run `remix collab init` first."
|
|
9389
|
-
});
|
|
9390
|
-
}
|
|
9391
|
-
const prompt = params.prompt.trim();
|
|
9392
|
-
const assistantResponse = params.assistantResponse.trim();
|
|
9393
|
-
if (!prompt) throw new RemixError("Prompt is required.", { exitCode: 2 });
|
|
9394
|
-
if (!assistantResponse) throw new RemixError("Assistant response is required.", { exitCode: 2 });
|
|
9395
|
-
const preflight = await collabRecordingPreflight({
|
|
9396
|
-
api: params.api,
|
|
9397
|
-
cwd: repoRoot,
|
|
9398
|
-
allowBranchMismatch: params.allowBranchMismatch
|
|
9162
|
+
const baseline = await readLocalBaseline({
|
|
9163
|
+
repoFingerprint: binding.repoFingerprint,
|
|
9164
|
+
laneId: binding.laneId,
|
|
9165
|
+
repoRoot
|
|
9399
9166
|
});
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
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.", {
|
|
9403
9169
|
exitCode: 2,
|
|
9404
|
-
hint: "
|
|
9170
|
+
hint: "Run `remix collab re-anchor` to create a fresh baseline."
|
|
9405
9171
|
});
|
|
9406
9172
|
}
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
allowBranchMismatch: params.allowBranchMismatch
|
|
9413
|
-
});
|
|
9414
|
-
}
|
|
9415
|
-
const branch = await getCurrentBranch(repoRoot);
|
|
9416
|
-
assertBoundBranchMatch({
|
|
9417
|
-
currentBranch: branch,
|
|
9418
|
-
branchName: binding.branchName,
|
|
9419
|
-
allowBranchMismatch: params.allowBranchMismatch,
|
|
9420
|
-
operation: "`remix collab record-turn`"
|
|
9173
|
+
const snapshot = await captureLocalSnapshot({
|
|
9174
|
+
repoRoot,
|
|
9175
|
+
repoFingerprint: binding.repoFingerprint,
|
|
9176
|
+
laneId: binding.laneId,
|
|
9177
|
+
branchName: binding.branchName
|
|
9421
9178
|
});
|
|
9422
|
-
const
|
|
9179
|
+
const mode = snapshot.snapshotHash === baseline.lastSnapshotHash ? "no_diff_turn" : "changed_turn";
|
|
9423
9180
|
const idempotencyKey = params.idempotencyKey?.trim() || buildDeterministicIdempotencyKey({
|
|
9181
|
+
kind: "collab_finalize_turn_boundary_v1",
|
|
9424
9182
|
appId: binding.currentAppId,
|
|
9425
|
-
|
|
9426
|
-
|
|
9183
|
+
laneId: binding.laneId,
|
|
9184
|
+
baselineSnapshotId: baseline.lastSnapshotId,
|
|
9185
|
+
baselineServerHeadHash: baseline.lastServerHeadHash,
|
|
9186
|
+
currentSnapshotId: snapshot.id,
|
|
9187
|
+
currentSnapshotHash: snapshot.snapshotHash,
|
|
9188
|
+
repoState: detected.repoState,
|
|
9427
9189
|
prompt,
|
|
9428
9190
|
assistantResponse
|
|
9429
9191
|
});
|
|
9430
|
-
const
|
|
9431
|
-
|
|
9432
|
-
|
|
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,
|
|
9433
9199
|
prompt,
|
|
9434
9200
|
assistantResponse,
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9201
|
+
baselineSnapshotId: baseline.lastSnapshotId,
|
|
9202
|
+
baselineServerHeadHash: baseline.lastServerHeadHash,
|
|
9203
|
+
currentSnapshotId: snapshot.id,
|
|
9204
|
+
idempotencyKey,
|
|
9205
|
+
metadata: {
|
|
9439
9206
|
remoteUrl: binding.remoteUrl,
|
|
9440
9207
|
defaultBranch: binding.defaultBranch,
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9208
|
+
actor: params.actor ?? null,
|
|
9209
|
+
repoState: detected.repoState
|
|
9210
|
+
}
|
|
9444
9211
|
});
|
|
9445
|
-
|
|
9446
|
-
|
|
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
|
+
};
|
|
9447
9223
|
}
|
|
9448
9224
|
|
|
9449
|
-
// node_modules/@remixhq/core/dist/chunk-
|
|
9225
|
+
// node_modules/@remixhq/core/dist/chunk-R7FVSCQW.js
|
|
9450
9226
|
async function readJsonSafe(res) {
|
|
9451
9227
|
const ct = res.headers.get("content-type") ?? "";
|
|
9452
9228
|
if (!ct.toLowerCase().includes("application/json")) return null;
|
|
@@ -9460,7 +9236,7 @@ function createApiClient(config, opts) {
|
|
|
9460
9236
|
const apiKey = (opts?.apiKey ?? "").trim();
|
|
9461
9237
|
const tokenProvider = opts?.tokenProvider;
|
|
9462
9238
|
const CLIENT_KEY_HEADER = "x-comerge-api-key";
|
|
9463
|
-
async function request(
|
|
9239
|
+
async function request(path12, init) {
|
|
9464
9240
|
if (!tokenProvider) {
|
|
9465
9241
|
throw new RemixError("API client is missing a token provider.", {
|
|
9466
9242
|
exitCode: 1,
|
|
@@ -9468,7 +9244,7 @@ function createApiClient(config, opts) {
|
|
|
9468
9244
|
});
|
|
9469
9245
|
}
|
|
9470
9246
|
const auth = await tokenProvider();
|
|
9471
|
-
const url = new URL(
|
|
9247
|
+
const url = new URL(path12, config.apiUrl).toString();
|
|
9472
9248
|
const doFetch = async (bearer) => fetch(url, {
|
|
9473
9249
|
...init,
|
|
9474
9250
|
headers: {
|
|
@@ -9492,7 +9268,7 @@ function createApiClient(config, opts) {
|
|
|
9492
9268
|
const json = await readJsonSafe(res);
|
|
9493
9269
|
return json ?? null;
|
|
9494
9270
|
}
|
|
9495
|
-
async function requestBinary(
|
|
9271
|
+
async function requestBinary(path12, init) {
|
|
9496
9272
|
if (!tokenProvider) {
|
|
9497
9273
|
throw new RemixError("API client is missing a token provider.", {
|
|
9498
9274
|
exitCode: 1,
|
|
@@ -9500,7 +9276,7 @@ function createApiClient(config, opts) {
|
|
|
9500
9276
|
});
|
|
9501
9277
|
}
|
|
9502
9278
|
const auth = await tokenProvider();
|
|
9503
|
-
const url = new URL(
|
|
9279
|
+
const url = new URL(path12, config.apiUrl).toString();
|
|
9504
9280
|
const doFetch = async (bearer) => fetch(url, {
|
|
9505
9281
|
...init,
|
|
9506
9282
|
headers: {
|
|
@@ -9599,6 +9375,15 @@ function createApiClient(config, opts) {
|
|
|
9599
9375
|
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
9600
9376
|
return request(`/v1/apps/${encodeURIComponent(appId)}/edit-queue${suffix}`, { method: "GET" });
|
|
9601
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
|
+
},
|
|
9602
9387
|
getMergeRequest: (mrId) => request(`/v1/merge-requests/${encodeURIComponent(mrId)}`, { method: "GET" }),
|
|
9603
9388
|
presignImportUpload: (payload) => request("/v1/apps/import/upload/presign", { method: "POST", body: JSON.stringify(payload) }),
|
|
9604
9389
|
importFromUpload: (payload) => request("/v1/apps/import/upload", { method: "POST", body: JSON.stringify(payload) }),
|
|
@@ -9606,6 +9391,11 @@ function createApiClient(config, opts) {
|
|
|
9606
9391
|
importFromUploadFirstParty: (payload) => request("/v1/apps/import/upload/first-party", { method: "POST", body: JSON.stringify(payload) }),
|
|
9607
9392
|
importFromGithubFirstParty: (payload) => request("/v1/apps/import/github/first-party", { method: "POST", body: JSON.stringify(payload) }),
|
|
9608
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
|
+
}),
|
|
9609
9399
|
downloadAppBundle: (appId) => requestBinary(`/v1/apps/${encodeURIComponent(appId)}/download.bundle`, { method: "GET" }),
|
|
9610
9400
|
createChangeStep: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/change-steps`, {
|
|
9611
9401
|
method: "POST",
|
|
@@ -10318,8 +10108,8 @@ function getErrorMap() {
|
|
|
10318
10108
|
|
|
10319
10109
|
// node_modules/zod/v3/helpers/parseUtil.js
|
|
10320
10110
|
var makeIssue = (params) => {
|
|
10321
|
-
const { data, path:
|
|
10322
|
-
const fullPath = [...
|
|
10111
|
+
const { data, path: path12, errorMaps, issueData } = params;
|
|
10112
|
+
const fullPath = [...path12, ...issueData.path || []];
|
|
10323
10113
|
const fullIssue = {
|
|
10324
10114
|
...issueData,
|
|
10325
10115
|
path: fullPath
|
|
@@ -10435,11 +10225,11 @@ var errorUtil;
|
|
|
10435
10225
|
|
|
10436
10226
|
// node_modules/zod/v3/types.js
|
|
10437
10227
|
var ParseInputLazyPath = class {
|
|
10438
|
-
constructor(parent, value,
|
|
10228
|
+
constructor(parent, value, path12, key) {
|
|
10439
10229
|
this._cachedPath = [];
|
|
10440
10230
|
this.parent = parent;
|
|
10441
10231
|
this.data = value;
|
|
10442
|
-
this._path =
|
|
10232
|
+
this._path = path12;
|
|
10443
10233
|
this._key = key;
|
|
10444
10234
|
}
|
|
10445
10235
|
get path() {
|
|
@@ -13882,8 +13672,8 @@ var coerce = {
|
|
|
13882
13672
|
var NEVER = INVALID;
|
|
13883
13673
|
|
|
13884
13674
|
// node_modules/@remixhq/core/dist/chunk-EVWDYCBL.js
|
|
13885
|
-
var
|
|
13886
|
-
var
|
|
13675
|
+
var import_promises17 = __toESM(require("fs/promises"), 1);
|
|
13676
|
+
var import_os3 = __toESM(require("os"), 1);
|
|
13887
13677
|
var import_path7 = __toESM(require("path"), 1);
|
|
13888
13678
|
|
|
13889
13679
|
// node_modules/tslib/tslib.es6.mjs
|
|
@@ -22772,8 +22562,8 @@ var IcebergError = class extends Error {
|
|
|
22772
22562
|
return this.status === 419;
|
|
22773
22563
|
}
|
|
22774
22564
|
};
|
|
22775
|
-
function buildUrl(baseUrl,
|
|
22776
|
-
const url = new URL(
|
|
22565
|
+
function buildUrl(baseUrl, path12, query) {
|
|
22566
|
+
const url = new URL(path12, baseUrl);
|
|
22777
22567
|
if (query) {
|
|
22778
22568
|
for (const [key, value] of Object.entries(query)) {
|
|
22779
22569
|
if (value !== void 0) {
|
|
@@ -22803,12 +22593,12 @@ function createFetchClient(options) {
|
|
|
22803
22593
|
return {
|
|
22804
22594
|
async request({
|
|
22805
22595
|
method,
|
|
22806
|
-
path:
|
|
22596
|
+
path: path12,
|
|
22807
22597
|
query,
|
|
22808
22598
|
body,
|
|
22809
22599
|
headers
|
|
22810
22600
|
}) {
|
|
22811
|
-
const url = buildUrl(options.baseUrl,
|
|
22601
|
+
const url = buildUrl(options.baseUrl, path12, query);
|
|
22812
22602
|
const authHeaders = await buildAuthHeaders(options.auth);
|
|
22813
22603
|
const res = await fetchFn(url, {
|
|
22814
22604
|
method,
|
|
@@ -23627,7 +23417,7 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
23627
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.
|
|
23628
23418
|
* @param fileBody The body of the file to be stored in the bucket.
|
|
23629
23419
|
*/
|
|
23630
|
-
async uploadOrUpdate(method,
|
|
23420
|
+
async uploadOrUpdate(method, path12, fileBody, fileOptions) {
|
|
23631
23421
|
var _this = this;
|
|
23632
23422
|
return _this.handleOperation(async () => {
|
|
23633
23423
|
let body;
|
|
@@ -23651,7 +23441,7 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
23651
23441
|
if ((typeof ReadableStream !== "undefined" && body instanceof ReadableStream || body && typeof body === "object" && "pipe" in body && typeof body.pipe === "function") && !options.duplex) options.duplex = "half";
|
|
23652
23442
|
}
|
|
23653
23443
|
if (fileOptions === null || fileOptions === void 0 ? void 0 : fileOptions.headers) headers = _objectSpread22(_objectSpread22({}, headers), fileOptions.headers);
|
|
23654
|
-
const cleanPath = _this._removeEmptyFolders(
|
|
23444
|
+
const cleanPath = _this._removeEmptyFolders(path12);
|
|
23655
23445
|
const _path = _this._getFinalPath(cleanPath);
|
|
23656
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 } : {}));
|
|
23657
23447
|
return {
|
|
@@ -23712,8 +23502,8 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
23712
23502
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
23713
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.
|
|
23714
23504
|
*/
|
|
23715
|
-
async upload(
|
|
23716
|
-
return this.uploadOrUpdate("POST",
|
|
23505
|
+
async upload(path12, fileBody, fileOptions) {
|
|
23506
|
+
return this.uploadOrUpdate("POST", path12, fileBody, fileOptions);
|
|
23717
23507
|
}
|
|
23718
23508
|
/**
|
|
23719
23509
|
* Upload a file with a token generated from `createSignedUploadUrl`.
|
|
@@ -23752,9 +23542,9 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
23752
23542
|
* - `objects` table permissions: none
|
|
23753
23543
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
23754
23544
|
*/
|
|
23755
|
-
async uploadToSignedUrl(
|
|
23545
|
+
async uploadToSignedUrl(path12, token, fileBody, fileOptions) {
|
|
23756
23546
|
var _this3 = this;
|
|
23757
|
-
const cleanPath = _this3._removeEmptyFolders(
|
|
23547
|
+
const cleanPath = _this3._removeEmptyFolders(path12);
|
|
23758
23548
|
const _path = _this3._getFinalPath(cleanPath);
|
|
23759
23549
|
const url = new URL(_this3.url + `/object/upload/sign/${_path}`);
|
|
23760
23550
|
url.searchParams.set("token", token);
|
|
@@ -23816,10 +23606,10 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
23816
23606
|
* - `objects` table permissions: `insert`
|
|
23817
23607
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
23818
23608
|
*/
|
|
23819
|
-
async createSignedUploadUrl(
|
|
23609
|
+
async createSignedUploadUrl(path12, options) {
|
|
23820
23610
|
var _this4 = this;
|
|
23821
23611
|
return _this4.handleOperation(async () => {
|
|
23822
|
-
let _path = _this4._getFinalPath(
|
|
23612
|
+
let _path = _this4._getFinalPath(path12);
|
|
23823
23613
|
const headers = _objectSpread22({}, _this4.headers);
|
|
23824
23614
|
if (options === null || options === void 0 ? void 0 : options.upsert) headers["x-upsert"] = "true";
|
|
23825
23615
|
const data = await post(_this4.fetch, `${_this4.url}/object/upload/sign/${_path}`, {}, { headers });
|
|
@@ -23828,7 +23618,7 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
23828
23618
|
if (!token) throw new StorageError("No token returned by API");
|
|
23829
23619
|
return {
|
|
23830
23620
|
signedUrl: url.toString(),
|
|
23831
|
-
path:
|
|
23621
|
+
path: path12,
|
|
23832
23622
|
token
|
|
23833
23623
|
};
|
|
23834
23624
|
});
|
|
@@ -23884,8 +23674,8 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
23884
23674
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
23885
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.
|
|
23886
23676
|
*/
|
|
23887
|
-
async update(
|
|
23888
|
-
return this.uploadOrUpdate("PUT",
|
|
23677
|
+
async update(path12, fileBody, fileOptions) {
|
|
23678
|
+
return this.uploadOrUpdate("PUT", path12, fileBody, fileOptions);
|
|
23889
23679
|
}
|
|
23890
23680
|
/**
|
|
23891
23681
|
* Moves an existing file to a new path in the same bucket.
|
|
@@ -24032,10 +23822,10 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
24032
23822
|
* - `objects` table permissions: `select`
|
|
24033
23823
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
24034
23824
|
*/
|
|
24035
|
-
async createSignedUrl(
|
|
23825
|
+
async createSignedUrl(path12, expiresIn, options) {
|
|
24036
23826
|
var _this8 = this;
|
|
24037
23827
|
return _this8.handleOperation(async () => {
|
|
24038
|
-
let _path = _this8._getFinalPath(
|
|
23828
|
+
let _path = _this8._getFinalPath(path12);
|
|
24039
23829
|
const hasTransform = typeof (options === null || options === void 0 ? void 0 : options.transform) === "object" && options.transform !== null && Object.keys(options.transform).length > 0;
|
|
24040
23830
|
let data = await post(_this8.fetch, `${_this8.url}/object/sign/${_path}`, _objectSpread22({ expiresIn }, hasTransform ? { transform: options.transform } : {}), { headers: _this8.headers });
|
|
24041
23831
|
const downloadQueryParam = (options === null || options === void 0 ? void 0 : options.download) ? `&download=${options.download === true ? "" : options.download}` : "";
|
|
@@ -24162,11 +23952,11 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
24162
23952
|
* - `objects` table permissions: `select`
|
|
24163
23953
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
24164
23954
|
*/
|
|
24165
|
-
download(
|
|
23955
|
+
download(path12, options, parameters) {
|
|
24166
23956
|
const renderPath = typeof (options === null || options === void 0 ? void 0 : options.transform) !== "undefined" ? "render/image/authenticated" : "object";
|
|
24167
23957
|
const transformationQuery = this.transformOptsToQueryString((options === null || options === void 0 ? void 0 : options.transform) || {});
|
|
24168
23958
|
const queryString = transformationQuery ? `?${transformationQuery}` : "";
|
|
24169
|
-
const _path = this._getFinalPath(
|
|
23959
|
+
const _path = this._getFinalPath(path12);
|
|
24170
23960
|
const downloadFn = () => get(this.fetch, `${this.url}/${renderPath}/${_path}${queryString}`, {
|
|
24171
23961
|
headers: this.headers,
|
|
24172
23962
|
noResolveJson: true
|
|
@@ -24196,9 +23986,9 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
24196
23986
|
* }
|
|
24197
23987
|
* ```
|
|
24198
23988
|
*/
|
|
24199
|
-
async info(
|
|
23989
|
+
async info(path12) {
|
|
24200
23990
|
var _this10 = this;
|
|
24201
|
-
const _path = _this10._getFinalPath(
|
|
23991
|
+
const _path = _this10._getFinalPath(path12);
|
|
24202
23992
|
return _this10.handleOperation(async () => {
|
|
24203
23993
|
return recursiveToCamel(await get(_this10.fetch, `${_this10.url}/object/info/${_path}`, { headers: _this10.headers }));
|
|
24204
23994
|
});
|
|
@@ -24218,9 +24008,9 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
24218
24008
|
* .exists('folder/avatar1.png')
|
|
24219
24009
|
* ```
|
|
24220
24010
|
*/
|
|
24221
|
-
async exists(
|
|
24011
|
+
async exists(path12) {
|
|
24222
24012
|
var _this11 = this;
|
|
24223
|
-
const _path = _this11._getFinalPath(
|
|
24013
|
+
const _path = _this11._getFinalPath(path12);
|
|
24224
24014
|
try {
|
|
24225
24015
|
await head(_this11.fetch, `${_this11.url}/object/${_path}`, { headers: _this11.headers });
|
|
24226
24016
|
return {
|
|
@@ -24297,8 +24087,8 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
24297
24087
|
* - `objects` table permissions: none
|
|
24298
24088
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
24299
24089
|
*/
|
|
24300
|
-
getPublicUrl(
|
|
24301
|
-
const _path = this._getFinalPath(
|
|
24090
|
+
getPublicUrl(path12, options) {
|
|
24091
|
+
const _path = this._getFinalPath(path12);
|
|
24302
24092
|
const _queryString = [];
|
|
24303
24093
|
const downloadQueryParam = (options === null || options === void 0 ? void 0 : options.download) ? `download=${options.download === true ? "" : options.download}` : "";
|
|
24304
24094
|
if (downloadQueryParam !== "") _queryString.push(downloadQueryParam);
|
|
@@ -24437,10 +24227,10 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
24437
24227
|
* - `objects` table permissions: `select`
|
|
24438
24228
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
24439
24229
|
*/
|
|
24440
|
-
async list(
|
|
24230
|
+
async list(path12, options, parameters) {
|
|
24441
24231
|
var _this13 = this;
|
|
24442
24232
|
return _this13.handleOperation(async () => {
|
|
24443
|
-
const body = _objectSpread22(_objectSpread22(_objectSpread22({}, DEFAULT_SEARCH_OPTIONS), options), {}, { prefix:
|
|
24233
|
+
const body = _objectSpread22(_objectSpread22(_objectSpread22({}, DEFAULT_SEARCH_OPTIONS), options), {}, { prefix: path12 || "" });
|
|
24444
24234
|
return await post(_this13.fetch, `${_this13.url}/object/list/${_this13.bucketId}`, body, { headers: _this13.headers }, parameters);
|
|
24445
24235
|
});
|
|
24446
24236
|
}
|
|
@@ -24504,11 +24294,11 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
24504
24294
|
if (typeof Buffer !== "undefined") return Buffer.from(data).toString("base64");
|
|
24505
24295
|
return btoa(data);
|
|
24506
24296
|
}
|
|
24507
|
-
_getFinalPath(
|
|
24508
|
-
return `${this.bucketId}/${
|
|
24297
|
+
_getFinalPath(path12) {
|
|
24298
|
+
return `${this.bucketId}/${path12.replace(/^\/+/, "")}`;
|
|
24509
24299
|
}
|
|
24510
|
-
_removeEmptyFolders(
|
|
24511
|
-
return
|
|
24300
|
+
_removeEmptyFolders(path12) {
|
|
24301
|
+
return path12.replace(/^\/|\/$/g, "").replace(/\/+/g, "/");
|
|
24512
24302
|
}
|
|
24513
24303
|
transformOptsToQueryString(transform) {
|
|
24514
24304
|
const params = [];
|
|
@@ -26222,7 +26012,7 @@ function decodeJWT(token) {
|
|
|
26222
26012
|
};
|
|
26223
26013
|
return data;
|
|
26224
26014
|
}
|
|
26225
|
-
async function
|
|
26015
|
+
async function sleep2(time) {
|
|
26226
26016
|
return await new Promise((accept) => {
|
|
26227
26017
|
setTimeout(() => accept(null), time);
|
|
26228
26018
|
});
|
|
@@ -31991,7 +31781,7 @@ var GoTrueClient = class _GoTrueClient {
|
|
|
31991
31781
|
const startedAt = Date.now();
|
|
31992
31782
|
return await retryable(async (attempt) => {
|
|
31993
31783
|
if (attempt > 0) {
|
|
31994
|
-
await
|
|
31784
|
+
await sleep2(200 * Math.pow(2, attempt - 1));
|
|
31995
31785
|
}
|
|
31996
31786
|
this._debug(debugName, "refreshing attempt", attempt);
|
|
31997
31787
|
return await _request(this.fetch, "POST", `${this.url}/token?grant_type=refresh_token`, {
|
|
@@ -33543,7 +33333,7 @@ var storedSessionSchema = external_exports.object({
|
|
|
33543
33333
|
function xdgConfigHome() {
|
|
33544
33334
|
const value = process.env.XDG_CONFIG_HOME;
|
|
33545
33335
|
if (typeof value === "string" && value.trim()) return value;
|
|
33546
|
-
return import_path7.default.join(
|
|
33336
|
+
return import_path7.default.join(import_os3.default.homedir(), ".config");
|
|
33547
33337
|
}
|
|
33548
33338
|
async function maybeLoadKeytar() {
|
|
33549
33339
|
try {
|
|
@@ -33562,21 +33352,21 @@ async function maybeLoadKeytar() {
|
|
|
33562
33352
|
}
|
|
33563
33353
|
async function ensurePathPermissions(filePath) {
|
|
33564
33354
|
const dir = import_path7.default.dirname(filePath);
|
|
33565
|
-
await
|
|
33355
|
+
await import_promises17.default.mkdir(dir, { recursive: true });
|
|
33566
33356
|
try {
|
|
33567
|
-
await
|
|
33357
|
+
await import_promises17.default.chmod(dir, 448);
|
|
33568
33358
|
} catch {
|
|
33569
33359
|
}
|
|
33570
33360
|
try {
|
|
33571
|
-
await
|
|
33361
|
+
await import_promises17.default.chmod(filePath, 384);
|
|
33572
33362
|
} catch {
|
|
33573
33363
|
}
|
|
33574
33364
|
}
|
|
33575
33365
|
async function writeJsonAtomic2(filePath, value) {
|
|
33576
|
-
await
|
|
33366
|
+
await import_promises17.default.mkdir(import_path7.default.dirname(filePath), { recursive: true });
|
|
33577
33367
|
const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
33578
|
-
await
|
|
33579
|
-
await
|
|
33368
|
+
await import_promises17.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
33369
|
+
await import_promises17.default.rename(tmpPath, filePath);
|
|
33580
33370
|
}
|
|
33581
33371
|
async function writeSessionFileFallback(filePath, session) {
|
|
33582
33372
|
await writeJsonAtomic2(filePath, session);
|
|
@@ -33599,7 +33389,7 @@ function createLocalSessionStore(params) {
|
|
|
33599
33389
|
return null;
|
|
33600
33390
|
}
|
|
33601
33391
|
}
|
|
33602
|
-
const raw = await
|
|
33392
|
+
const raw = await import_promises17.default.readFile(filePath, "utf8").catch(() => null);
|
|
33603
33393
|
if (!raw) return null;
|
|
33604
33394
|
try {
|
|
33605
33395
|
const parsed = storedSessionSchema.safeParse(JSON.parse(raw));
|
|
@@ -33786,12 +33576,12 @@ async function createHookCollabApiClient() {
|
|
|
33786
33576
|
|
|
33787
33577
|
// src/hook-diagnostics.ts
|
|
33788
33578
|
var import_node_crypto2 = require("crypto");
|
|
33789
|
-
var
|
|
33579
|
+
var import_promises19 = __toESM(require("fs/promises"), 1);
|
|
33790
33580
|
var import_node_os5 = __toESM(require("os"), 1);
|
|
33791
33581
|
var import_node_path7 = __toESM(require("path"), 1);
|
|
33792
33582
|
|
|
33793
33583
|
// src/hook-state.ts
|
|
33794
|
-
var
|
|
33584
|
+
var import_promises18 = __toESM(require("fs/promises"), 1);
|
|
33795
33585
|
var import_node_os4 = __toESM(require("os"), 1);
|
|
33796
33586
|
var import_node_path6 = __toESM(require("path"), 1);
|
|
33797
33587
|
var import_node_crypto = require("crypto");
|
|
@@ -33809,20 +33599,20 @@ function stateLockMetaPath(sessionId) {
|
|
|
33809
33599
|
return import_node_path6.default.join(stateLockPath(sessionId), "owner.json");
|
|
33810
33600
|
}
|
|
33811
33601
|
async function writeJsonAtomic3(filePath, value) {
|
|
33812
|
-
await
|
|
33602
|
+
await import_promises18.default.mkdir(import_node_path6.default.dirname(filePath), { recursive: true });
|
|
33813
33603
|
const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
33814
|
-
await
|
|
33815
|
-
await
|
|
33604
|
+
await import_promises18.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
33605
|
+
await import_promises18.default.rename(tmpPath, filePath);
|
|
33816
33606
|
}
|
|
33817
33607
|
var STATE_LOCK_WAIT_MS = 2e3;
|
|
33818
33608
|
var STATE_LOCK_POLL_MS = 25;
|
|
33819
33609
|
var STATE_LOCK_STALE_MS = 3e4;
|
|
33820
33610
|
var STATE_LOCK_HEARTBEAT_MS = 5e3;
|
|
33821
|
-
async function
|
|
33611
|
+
async function sleep3(ms) {
|
|
33822
33612
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
33823
33613
|
}
|
|
33824
33614
|
async function readStateLockMetadata(sessionId) {
|
|
33825
|
-
const raw = await
|
|
33615
|
+
const raw = await import_promises18.default.readFile(stateLockMetaPath(sessionId), "utf8").catch(() => null);
|
|
33826
33616
|
if (!raw) return null;
|
|
33827
33617
|
try {
|
|
33828
33618
|
const parsed = JSON.parse(raw);
|
|
@@ -33847,13 +33637,13 @@ async function tryRemoveStaleStateLock(sessionId) {
|
|
|
33847
33637
|
const metadata = await readStateLockMetadata(sessionId);
|
|
33848
33638
|
const staleByHeartbeat = metadata && Date.now() - new Date(metadata.heartbeatAt).getTime() > STATE_LOCK_STALE_MS;
|
|
33849
33639
|
if (staleByHeartbeat) {
|
|
33850
|
-
await
|
|
33640
|
+
await import_promises18.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
|
|
33851
33641
|
return true;
|
|
33852
33642
|
}
|
|
33853
33643
|
if (!metadata) {
|
|
33854
|
-
const lockStat = await
|
|
33644
|
+
const lockStat = await import_promises18.default.stat(lockPath).catch(() => null);
|
|
33855
33645
|
if (lockStat && Date.now() - lockStat.mtimeMs > STATE_LOCK_STALE_MS) {
|
|
33856
|
-
await
|
|
33646
|
+
await import_promises18.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
|
|
33857
33647
|
return true;
|
|
33858
33648
|
}
|
|
33859
33649
|
}
|
|
@@ -33862,10 +33652,10 @@ async function tryRemoveStaleStateLock(sessionId) {
|
|
|
33862
33652
|
async function acquireStateLock(sessionId) {
|
|
33863
33653
|
const lockPath = stateLockPath(sessionId);
|
|
33864
33654
|
const deadline = Date.now() + STATE_LOCK_WAIT_MS;
|
|
33865
|
-
await
|
|
33655
|
+
await import_promises18.default.mkdir(stateRoot(), { recursive: true });
|
|
33866
33656
|
while (true) {
|
|
33867
33657
|
try {
|
|
33868
|
-
await
|
|
33658
|
+
await import_promises18.default.mkdir(lockPath);
|
|
33869
33659
|
const ownerId = (0, import_node_crypto.randomUUID)();
|
|
33870
33660
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
33871
33661
|
const metadata = {
|
|
@@ -33890,7 +33680,7 @@ async function acquireStateLock(sessionId) {
|
|
|
33890
33680
|
clearInterval(heartbeat);
|
|
33891
33681
|
const currentMetadata = await readStateLockMetadata(sessionId);
|
|
33892
33682
|
if (currentMetadata?.ownerId === ownerId) {
|
|
33893
|
-
await
|
|
33683
|
+
await import_promises18.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
|
|
33894
33684
|
}
|
|
33895
33685
|
};
|
|
33896
33686
|
} catch (error) {
|
|
@@ -33904,7 +33694,7 @@ async function acquireStateLock(sessionId) {
|
|
|
33904
33694
|
if (Date.now() >= deadline) {
|
|
33905
33695
|
throw new Error(`Timed out acquiring hook state lock for session ${sessionId}.`);
|
|
33906
33696
|
}
|
|
33907
|
-
await
|
|
33697
|
+
await sleep3(STATE_LOCK_POLL_MS);
|
|
33908
33698
|
}
|
|
33909
33699
|
}
|
|
33910
33700
|
}
|
|
@@ -33930,6 +33720,12 @@ function normalizeStringArray(value) {
|
|
|
33930
33720
|
)
|
|
33931
33721
|
);
|
|
33932
33722
|
}
|
|
33723
|
+
function normalizeManualRecordingScope(value) {
|
|
33724
|
+
if (value === "full_turn") {
|
|
33725
|
+
return "full_turn";
|
|
33726
|
+
}
|
|
33727
|
+
return null;
|
|
33728
|
+
}
|
|
33933
33729
|
function normalizeTouchedRepo(value, repoRoot) {
|
|
33934
33730
|
if (!value || typeof value !== "object") return null;
|
|
33935
33731
|
const parsed = value;
|
|
@@ -33948,7 +33744,7 @@ function normalizeTouchedRepo(value, repoRoot) {
|
|
|
33948
33744
|
manuallyRecorded: Boolean(parsed.manuallyRecorded),
|
|
33949
33745
|
manuallyRecordedAt: normalizeString(parsed.manuallyRecordedAt),
|
|
33950
33746
|
manuallyRecordedByTool: normalizeString(parsed.manuallyRecordedByTool),
|
|
33951
|
-
manualRecordingScope: parsed.manualRecordingScope
|
|
33747
|
+
manualRecordingScope: normalizeManualRecordingScope(parsed.manualRecordingScope),
|
|
33952
33748
|
manualRemoteChangeRecordedAt: normalizeString(parsed.manualRemoteChangeRecordedAt),
|
|
33953
33749
|
stopAttempted: Boolean(parsed.stopAttempted),
|
|
33954
33750
|
stopRecorded: Boolean(parsed.stopRecorded),
|
|
@@ -34002,7 +33798,7 @@ async function updatePendingTurnState(sessionId, updater) {
|
|
|
34002
33798
|
});
|
|
34003
33799
|
}
|
|
34004
33800
|
async function loadPendingTurnState(sessionId) {
|
|
34005
|
-
const raw = await
|
|
33801
|
+
const raw = await import_promises18.default.readFile(statePath(sessionId), "utf8").catch(() => null);
|
|
34006
33802
|
if (!raw) return null;
|
|
34007
33803
|
try {
|
|
34008
33804
|
const parsed = JSON.parse(raw);
|
|
@@ -34104,14 +33900,14 @@ async function listTouchedRepos(sessionId) {
|
|
|
34104
33900
|
}
|
|
34105
33901
|
async function clearPendingTurnState(sessionId) {
|
|
34106
33902
|
await withStateLock(sessionId, async () => {
|
|
34107
|
-
await
|
|
33903
|
+
await import_promises18.default.rm(statePath(sessionId), { force: true }).catch(() => void 0);
|
|
34108
33904
|
});
|
|
34109
33905
|
}
|
|
34110
33906
|
|
|
34111
33907
|
// package.json
|
|
34112
33908
|
var package_default = {
|
|
34113
33909
|
name: "@remixhq/claude-plugin",
|
|
34114
|
-
version: "0.1.
|
|
33910
|
+
version: "0.1.18",
|
|
34115
33911
|
description: "Claude Code plugin for Remix collaboration workflows",
|
|
34116
33912
|
homepage: "https://github.com/RemixDotOne/remix-claude-plugin",
|
|
34117
33913
|
license: "MIT",
|
|
@@ -34142,8 +33938,8 @@ var package_default = {
|
|
|
34142
33938
|
prepack: "npm run build"
|
|
34143
33939
|
},
|
|
34144
33940
|
dependencies: {
|
|
34145
|
-
"@remixhq/core": "^0.1.
|
|
34146
|
-
"@remixhq/mcp": "^0.1.
|
|
33941
|
+
"@remixhq/core": "^0.1.13",
|
|
33942
|
+
"@remixhq/mcp": "^0.1.13"
|
|
34147
33943
|
},
|
|
34148
33944
|
devDependencies: {
|
|
34149
33945
|
"@types/node": "^25.4.0",
|
|
@@ -34194,13 +33990,13 @@ function normalizeFields(fields) {
|
|
|
34194
33990
|
return Object.fromEntries(normalizedEntries);
|
|
34195
33991
|
}
|
|
34196
33992
|
async function rotateLogIfNeeded(logPath) {
|
|
34197
|
-
const stat = await
|
|
33993
|
+
const stat = await import_promises19.default.stat(logPath).catch(() => null);
|
|
34198
33994
|
if (!stat || stat.size < MAX_LOG_BYTES) {
|
|
34199
33995
|
return;
|
|
34200
33996
|
}
|
|
34201
33997
|
const rotatedPath = `${logPath}.1`;
|
|
34202
|
-
await
|
|
34203
|
-
await
|
|
33998
|
+
await import_promises19.default.rm(rotatedPath, { force: true }).catch(() => void 0);
|
|
33999
|
+
await import_promises19.default.rename(logPath, rotatedPath).catch(() => void 0);
|
|
34204
34000
|
}
|
|
34205
34001
|
function summarizeText(value) {
|
|
34206
34002
|
if (typeof value !== "string" || !value.trim()) {
|
|
@@ -34220,7 +34016,7 @@ function summarizeText(value) {
|
|
|
34220
34016
|
async function appendHookDiagnosticsEvent(params) {
|
|
34221
34017
|
try {
|
|
34222
34018
|
const logPath = getHookDiagnosticsLogPath();
|
|
34223
|
-
await
|
|
34019
|
+
await import_promises19.default.mkdir(import_node_path7.default.dirname(logPath), { recursive: true });
|
|
34224
34020
|
await rotateLogIfNeeded(logPath);
|
|
34225
34021
|
const event = {
|
|
34226
34022
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -34237,14 +34033,14 @@ async function appendHookDiagnosticsEvent(params) {
|
|
|
34237
34033
|
message: params.message?.trim() || null,
|
|
34238
34034
|
fields: normalizeFields(params.fields)
|
|
34239
34035
|
};
|
|
34240
|
-
await
|
|
34036
|
+
await import_promises19.default.appendFile(logPath, `${JSON.stringify(event)}
|
|
34241
34037
|
`, "utf8");
|
|
34242
34038
|
} catch {
|
|
34243
34039
|
}
|
|
34244
34040
|
}
|
|
34245
34041
|
|
|
34246
34042
|
// src/hook-utils.ts
|
|
34247
|
-
var
|
|
34043
|
+
var import_promises20 = __toESM(require("fs/promises"), 1);
|
|
34248
34044
|
var import_node_path8 = __toESM(require("path"), 1);
|
|
34249
34045
|
async function readJsonStdin() {
|
|
34250
34046
|
const chunks = [];
|
|
@@ -34308,13 +34104,13 @@ function extractBoolean(input, keys) {
|
|
|
34308
34104
|
async function findBoundRepo(startPath) {
|
|
34309
34105
|
if (!startPath) return null;
|
|
34310
34106
|
let current = import_node_path8.default.resolve(startPath);
|
|
34311
|
-
let stats = await
|
|
34107
|
+
let stats = await import_promises20.default.stat(current).catch(() => null);
|
|
34312
34108
|
if (stats?.isFile()) {
|
|
34313
34109
|
current = import_node_path8.default.dirname(current);
|
|
34314
34110
|
}
|
|
34315
34111
|
while (true) {
|
|
34316
34112
|
const bindingPath = import_node_path8.default.join(current, ".remix", "config.json");
|
|
34317
|
-
const bindingStats = await
|
|
34113
|
+
const bindingStats = await import_promises20.default.stat(bindingPath).catch(() => null);
|
|
34318
34114
|
if (bindingStats?.isFile()) return current;
|
|
34319
34115
|
const parent = import_node_path8.default.dirname(current);
|
|
34320
34116
|
if (parent === current) return null;
|
|
@@ -34341,6 +34137,7 @@ var HOOK_ACTOR = {
|
|
|
34341
34137
|
version: pluginMetadata.version,
|
|
34342
34138
|
provider: "anthropic"
|
|
34343
34139
|
};
|
|
34140
|
+
var collabFinalizeTurn2 = collabFinalizeTurn;
|
|
34344
34141
|
function getErrorDetails(error) {
|
|
34345
34142
|
if (error instanceof Error) {
|
|
34346
34143
|
const hint = typeof error.hint === "string" ? String(error.hint) : null;
|
|
@@ -34352,58 +34149,13 @@ function getErrorDetails(error) {
|
|
|
34352
34149
|
const message = typeof error === "string" && error.trim() ? error.trim() : "Fallback Remix turn recording failed.";
|
|
34353
34150
|
return { message, hint: null };
|
|
34354
34151
|
}
|
|
34355
|
-
function getRecordingBlockedMessage(status, repoRoot) {
|
|
34356
|
-
if (status.status === "branch_binding_missing") {
|
|
34357
|
-
return {
|
|
34358
|
-
message: "Fallback Remix turn recording was blocked because the current branch does not have a Remix lane binding yet.",
|
|
34359
|
-
hint: status.hint || `Run \`remix_collab_status\` for ${repoRoot}, then initialize or provision the current branch lane before recording work.`
|
|
34360
|
-
};
|
|
34361
|
-
}
|
|
34362
|
-
if (status.status === "family_ambiguous") {
|
|
34363
|
-
return {
|
|
34364
|
-
message: "Fallback Remix turn recording was blocked because multiple canonical Remix families match this repository and the current checkout does not identify which family to use.",
|
|
34365
|
-
hint: status.hint || `Continue from a checkout already bound to the intended family, or run \`remix_collab_init\` with forceNew=true for ${repoRoot} to create a new canonical family.`
|
|
34366
|
-
};
|
|
34367
|
-
}
|
|
34368
|
-
switch (status.status) {
|
|
34369
|
-
case "not_git_repo":
|
|
34370
|
-
return {
|
|
34371
|
-
message: "Fallback Remix turn recording failed because the repository is no longer inside a git repository.",
|
|
34372
|
-
hint: status.hint || `Repo root: ${repoRoot}`
|
|
34373
|
-
};
|
|
34374
|
-
case "not_bound":
|
|
34375
|
-
return {
|
|
34376
|
-
message: "Fallback Remix turn recording failed because the repository is no longer bound to Remix.",
|
|
34377
|
-
hint: status.hint || `Repo root: ${repoRoot}`
|
|
34378
|
-
};
|
|
34379
|
-
case "missing_head":
|
|
34380
|
-
return {
|
|
34381
|
-
message: "Fallback Remix turn recording failed because the repository HEAD could not be resolved.",
|
|
34382
|
-
hint: status.hint || `Repo root: ${repoRoot}`
|
|
34383
|
-
};
|
|
34384
|
-
case "branch_mismatch":
|
|
34385
|
-
return {
|
|
34386
|
-
message: "Fallback Remix turn recording was blocked because the current checkout branch does not match the branch expected by the bound Remix lane.",
|
|
34387
|
-
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.`
|
|
34388
|
-
};
|
|
34389
|
-
case "metadata_conflict":
|
|
34390
|
-
return {
|
|
34391
|
-
message: "Fallback Remix turn recording was blocked because local repository metadata conflicts with the bound Remix app.",
|
|
34392
|
-
hint: status.hint || `Repo root: ${repoRoot}`
|
|
34393
|
-
};
|
|
34394
|
-
case "reconcile_required":
|
|
34395
|
-
return {
|
|
34396
|
-
message: "Fallback Remix turn recording was blocked because the repository must be reconciled before recording can continue safely.",
|
|
34397
|
-
hint: status.hint || `Repo root: ${repoRoot}`
|
|
34398
|
-
};
|
|
34399
|
-
default:
|
|
34400
|
-
return null;
|
|
34401
|
-
}
|
|
34402
|
-
}
|
|
34403
34152
|
function buildRepoIdempotencyKey(turnId, repo) {
|
|
34404
34153
|
const repoToken = repo.currentAppId?.trim() || repo.repoRoot;
|
|
34405
34154
|
return `${turnId}:${repoToken}:finalize_turn`;
|
|
34406
34155
|
}
|
|
34156
|
+
function isLegacyManualRecordingTool(toolName) {
|
|
34157
|
+
return /remix_collab_(add|add_change_step|record_turn|record_no_diff_turn)$/i.test(toolName ?? "");
|
|
34158
|
+
}
|
|
34407
34159
|
function shouldSkipStopRecording(repo) {
|
|
34408
34160
|
if (repo.stopRecorded) {
|
|
34409
34161
|
return true;
|
|
@@ -34411,7 +34163,8 @@ function shouldSkipStopRecording(repo) {
|
|
|
34411
34163
|
if (!repo.manuallyRecorded) {
|
|
34412
34164
|
return false;
|
|
34413
34165
|
}
|
|
34414
|
-
|
|
34166
|
+
const alreadyRecordedByCompatibleFlow = repo.manualRecordingScope === "full_turn" || isLegacyManualRecordingTool(repo.manuallyRecordedByTool);
|
|
34167
|
+
if (!alreadyRecordedByCompatibleFlow) {
|
|
34415
34168
|
return false;
|
|
34416
34169
|
}
|
|
34417
34170
|
if (!repo.manuallyRecordedAt) {
|
|
@@ -34422,12 +34175,6 @@ function shouldSkipStopRecording(repo) {
|
|
|
34422
34175
|
}
|
|
34423
34176
|
return new Date(repo.lastObservedWriteAt).getTime() <= new Date(repo.manuallyRecordedAt).getTime();
|
|
34424
34177
|
}
|
|
34425
|
-
function hasNewObservedWriteSince(timestamp, repo) {
|
|
34426
|
-
if (!repo.lastObservedWriteAt) {
|
|
34427
|
-
return false;
|
|
34428
|
-
}
|
|
34429
|
-
return new Date(repo.lastObservedWriteAt).getTime() > new Date(timestamp).getTime();
|
|
34430
|
-
}
|
|
34431
34178
|
function createFallbackTouchedRepo(params) {
|
|
34432
34179
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
34433
34180
|
return {
|
|
@@ -34486,113 +34233,9 @@ async function recordTouchedRepo(params) {
|
|
|
34486
34233
|
reason: "repo_not_bound",
|
|
34487
34234
|
repoRoot: repo.repoRoot
|
|
34488
34235
|
});
|
|
34489
|
-
return false;
|
|
34490
|
-
}
|
|
34491
|
-
const workspaceDiff = await getWorkspaceDiff(repo.repoRoot);
|
|
34492
|
-
if (workspaceDiff.diff.trim()) {
|
|
34493
|
-
await appendHookDiagnosticsEvent({
|
|
34494
|
-
hook,
|
|
34495
|
-
sessionId,
|
|
34496
|
-
turnId,
|
|
34497
|
-
stage: "workspace_diff_checked",
|
|
34498
|
-
result: "info",
|
|
34499
|
-
repoRoot: repo.repoRoot,
|
|
34500
|
-
fields: {
|
|
34501
|
-
hasWorkspaceDiff: true,
|
|
34502
|
-
diffLength: workspaceDiff.diff.length
|
|
34503
|
-
}
|
|
34504
|
-
});
|
|
34505
|
-
if (repo.manualRemoteChangeRecordedAt && !hasNewObservedWriteSince(repo.manualRemoteChangeRecordedAt, repo)) {
|
|
34506
|
-
await markTouchedRepoStopRecorded(sessionId, repo.repoRoot, { mode: "changed_turn" });
|
|
34507
|
-
await appendHookDiagnosticsEvent({
|
|
34508
|
-
hook,
|
|
34509
|
-
sessionId,
|
|
34510
|
-
turnId,
|
|
34511
|
-
stage: "recording_skipped",
|
|
34512
|
-
result: "success",
|
|
34513
|
-
reason: "manual_recording_already_covers_diff",
|
|
34514
|
-
repoRoot: repo.repoRoot
|
|
34515
|
-
});
|
|
34516
|
-
return true;
|
|
34517
|
-
}
|
|
34518
|
-
const recordingPreflight2 = await collabRecordingPreflight({
|
|
34519
|
-
api,
|
|
34520
|
-
cwd: repo.repoRoot
|
|
34521
|
-
});
|
|
34522
|
-
const blocked2 = getRecordingBlockedMessage(recordingPreflight2, repo.repoRoot);
|
|
34523
|
-
if (blocked2) {
|
|
34524
|
-
await markTouchedRepoRecordingFailure(sessionId, repo.repoRoot, blocked2);
|
|
34525
|
-
await appendHookDiagnosticsEvent({
|
|
34526
|
-
hook,
|
|
34527
|
-
sessionId,
|
|
34528
|
-
turnId,
|
|
34529
|
-
stage: "recording_preflight",
|
|
34530
|
-
result: "error",
|
|
34531
|
-
reason: recordingPreflight2.status,
|
|
34532
|
-
repoRoot: repo.repoRoot,
|
|
34533
|
-
message: blocked2.message,
|
|
34534
|
-
fields: {
|
|
34535
|
-
hint: blocked2.hint
|
|
34536
|
-
}
|
|
34537
|
-
});
|
|
34538
|
-
return false;
|
|
34539
|
-
}
|
|
34540
|
-
await collabAdd({
|
|
34541
|
-
api,
|
|
34542
|
-
cwd: repo.repoRoot,
|
|
34543
|
-
prompt,
|
|
34544
|
-
assistantResponse,
|
|
34545
|
-
diffSource: "worktree",
|
|
34546
|
-
idempotencyKey: buildRepoIdempotencyKey(turnId, repo),
|
|
34547
|
-
actor: HOOK_ACTOR
|
|
34548
|
-
});
|
|
34549
|
-
await markTouchedRepoStopRecorded(sessionId, repo.repoRoot, { mode: "changed_turn" });
|
|
34550
|
-
await appendHookDiagnosticsEvent({
|
|
34551
|
-
hook,
|
|
34552
|
-
sessionId,
|
|
34553
|
-
turnId,
|
|
34554
|
-
stage: "recording_completed",
|
|
34555
|
-
result: "success",
|
|
34556
|
-
reason: "changed_turn_recorded",
|
|
34557
|
-
repoRoot: repo.repoRoot
|
|
34558
|
-
});
|
|
34559
|
-
return true;
|
|
34560
|
-
}
|
|
34561
|
-
await appendHookDiagnosticsEvent({
|
|
34562
|
-
hook,
|
|
34563
|
-
sessionId,
|
|
34564
|
-
turnId,
|
|
34565
|
-
stage: "workspace_diff_checked",
|
|
34566
|
-
result: "info",
|
|
34567
|
-
repoRoot: repo.repoRoot,
|
|
34568
|
-
fields: {
|
|
34569
|
-
hasWorkspaceDiff: false,
|
|
34570
|
-
diffLength: 0
|
|
34571
|
-
}
|
|
34572
|
-
});
|
|
34573
|
-
const recordingPreflight = await collabRecordingPreflight({
|
|
34574
|
-
api,
|
|
34575
|
-
cwd: repo.repoRoot
|
|
34576
|
-
});
|
|
34577
|
-
const blocked = getRecordingBlockedMessage(recordingPreflight, repo.repoRoot);
|
|
34578
|
-
if (blocked) {
|
|
34579
|
-
await markTouchedRepoRecordingFailure(sessionId, repo.repoRoot, blocked);
|
|
34580
|
-
await appendHookDiagnosticsEvent({
|
|
34581
|
-
hook,
|
|
34582
|
-
sessionId,
|
|
34583
|
-
turnId,
|
|
34584
|
-
stage: "recording_preflight",
|
|
34585
|
-
result: "error",
|
|
34586
|
-
reason: recordingPreflight.status,
|
|
34587
|
-
repoRoot: repo.repoRoot,
|
|
34588
|
-
message: blocked.message,
|
|
34589
|
-
fields: {
|
|
34590
|
-
hint: blocked.hint
|
|
34591
|
-
}
|
|
34592
|
-
});
|
|
34593
|
-
return false;
|
|
34236
|
+
return { recorded: false, queued: false };
|
|
34594
34237
|
}
|
|
34595
|
-
await
|
|
34238
|
+
const result = await collabFinalizeTurn2({
|
|
34596
34239
|
api,
|
|
34597
34240
|
cwd: repo.repoRoot,
|
|
34598
34241
|
prompt,
|
|
@@ -34600,17 +34243,17 @@ async function recordTouchedRepo(params) {
|
|
|
34600
34243
|
idempotencyKey: buildRepoIdempotencyKey(turnId, repo),
|
|
34601
34244
|
actor: HOOK_ACTOR
|
|
34602
34245
|
});
|
|
34603
|
-
await markTouchedRepoStopRecorded(sessionId, repo.repoRoot, { mode:
|
|
34246
|
+
await markTouchedRepoStopRecorded(sessionId, repo.repoRoot, { mode: result.mode });
|
|
34604
34247
|
await appendHookDiagnosticsEvent({
|
|
34605
34248
|
hook,
|
|
34606
34249
|
sessionId,
|
|
34607
34250
|
turnId,
|
|
34608
34251
|
stage: "recording_completed",
|
|
34609
34252
|
result: "success",
|
|
34610
|
-
reason:
|
|
34253
|
+
reason: result.mode,
|
|
34611
34254
|
repoRoot: repo.repoRoot
|
|
34612
34255
|
});
|
|
34613
|
-
return true;
|
|
34256
|
+
return { recorded: true, queued: result.queued === true };
|
|
34614
34257
|
} catch (error) {
|
|
34615
34258
|
const details = getErrorDetails(error);
|
|
34616
34259
|
await markTouchedRepoRecordingFailure(sessionId, repo.repoRoot, details);
|
|
@@ -34627,9 +34270,19 @@ async function recordTouchedRepo(params) {
|
|
|
34627
34270
|
hint: details.hint
|
|
34628
34271
|
}
|
|
34629
34272
|
});
|
|
34630
|
-
return false;
|
|
34273
|
+
return { recorded: false, queued: false };
|
|
34631
34274
|
}
|
|
34632
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
|
+
}
|
|
34633
34286
|
async function runHookStopCollab(payload) {
|
|
34634
34287
|
const hook = "Stop";
|
|
34635
34288
|
if (extractBoolean(payload, ["stop_hook_active"])) {
|
|
@@ -34776,8 +34429,11 @@ async function runHookStopCollab(payload) {
|
|
|
34776
34429
|
}
|
|
34777
34430
|
});
|
|
34778
34431
|
let hadFailure = false;
|
|
34432
|
+
let queuedFinalizeWork = false;
|
|
34779
34433
|
for (const repo of touchedRepos) {
|
|
34780
34434
|
if (shouldSkipStopRecording(repo)) {
|
|
34435
|
+
const backupDrainQueued = repo.manuallyRecordedByTool === "remix_collab_finalize_turn" && repo.manualRecordingScope === "full_turn";
|
|
34436
|
+
queuedFinalizeWork = queuedFinalizeWork || backupDrainQueued;
|
|
34781
34437
|
await appendHookDiagnosticsEvent({
|
|
34782
34438
|
hook,
|
|
34783
34439
|
sessionId,
|
|
@@ -34788,12 +34444,13 @@ async function runHookStopCollab(payload) {
|
|
|
34788
34444
|
repoRoot: repo.repoRoot,
|
|
34789
34445
|
fields: {
|
|
34790
34446
|
manuallyRecorded: repo.manuallyRecorded,
|
|
34791
|
-
stopRecorded: repo.stopRecorded
|
|
34447
|
+
stopRecorded: repo.stopRecorded,
|
|
34448
|
+
backupDrainQueued
|
|
34792
34449
|
}
|
|
34793
34450
|
});
|
|
34794
34451
|
continue;
|
|
34795
34452
|
}
|
|
34796
|
-
const
|
|
34453
|
+
const recording = await recordTouchedRepo({
|
|
34797
34454
|
hook,
|
|
34798
34455
|
sessionId,
|
|
34799
34456
|
turnId: state.turnId,
|
|
@@ -34802,10 +34459,14 @@ async function runHookStopCollab(payload) {
|
|
|
34802
34459
|
assistantResponse,
|
|
34803
34460
|
api
|
|
34804
34461
|
});
|
|
34805
|
-
|
|
34462
|
+
queuedFinalizeWork = queuedFinalizeWork || recording.queued;
|
|
34463
|
+
if (!recording.recorded) {
|
|
34806
34464
|
hadFailure = true;
|
|
34807
34465
|
}
|
|
34808
34466
|
}
|
|
34467
|
+
if (queuedFinalizeWork) {
|
|
34468
|
+
spawnFinalizeQueueDrainer();
|
|
34469
|
+
}
|
|
34809
34470
|
if (!hadFailure) {
|
|
34810
34471
|
await clearPendingTurnState(sessionId);
|
|
34811
34472
|
await appendHookDiagnosticsEvent({
|
|
@@ -34844,6 +34505,14 @@ async function runHookStopCollab(payload) {
|
|
|
34844
34505
|
}
|
|
34845
34506
|
}
|
|
34846
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
|
+
}
|
|
34847
34516
|
const payload = await readJsonStdin();
|
|
34848
34517
|
await runHookStopCollab(payload);
|
|
34849
34518
|
}
|