@ricsam/r5d-worker 0.0.45 → 0.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/main.cjs +308 -131
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/workspace-sync.cjs +187 -49
- package/dist/mjs/main.mjs +308 -131
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/workspace-sync.mjs +187 -49
- package/dist/types/main.d.ts +19 -8
- package/dist/types/workspace-sync.d.ts +3 -0
- package/package.json +1 -1
package/dist/mjs/main.mjs
CHANGED
|
@@ -246,7 +246,25 @@ async function readResponseText(response) {
|
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
248
|
const R5D_ARTIFACTS_DIR_REF = "$R5D_ARTIFACTS_DIR";
|
|
249
|
+
const R5D_PLANS_DIR_REF = "$R5D_PLANS_DIR";
|
|
250
|
+
const R5D_ACTIVE_PLAN_FILE_REF = "$R5D_ACTIVE_PLAN_FILE";
|
|
249
251
|
const R5D_PLANS_DIR_ENV = "R5D_PLANS_DIR";
|
|
252
|
+
const BUILT_IN_TOOL_PATH_REFS = [R5D_ARTIFACTS_DIR_REF, R5D_PLANS_DIR_REF, R5D_ACTIVE_PLAN_FILE_REF];
|
|
253
|
+
function parseBuiltInToolPath(inputPath) {
|
|
254
|
+
const normalized = inputPath.replace(/\\/g, "/");
|
|
255
|
+
const match = /^\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))(?:\/(.*))?$/.exec(normalized);
|
|
256
|
+
if (!match) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
const ref = `$${match[1] ?? match[2]}`;
|
|
260
|
+
if (!BUILT_IN_TOOL_PATH_REFS.includes(ref)) {
|
|
261
|
+
throw new Error(`Unsupported tool path variable "${ref}". Supported variables: ${BUILT_IN_TOOL_PATH_REFS.join(", ")}.`);
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
ref,
|
|
265
|
+
relativePath: match[3] ?? ""
|
|
266
|
+
};
|
|
267
|
+
}
|
|
250
268
|
function validateArtifactSessionId(sessionId) {
|
|
251
269
|
if (!/^(migration-)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) {
|
|
252
270
|
throw new Error(`Invalid artifact session id: ${sessionId}`);
|
|
@@ -255,7 +273,7 @@ function validateArtifactSessionId(sessionId) {
|
|
|
255
273
|
function assertInsideRoot(rootPath, candidatePath, label) {
|
|
256
274
|
const relative = path.relative(rootPath, candidatePath);
|
|
257
275
|
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
258
|
-
throw new Error(`${label} escapes
|
|
276
|
+
throw new Error(`${label} escapes its allowed root`);
|
|
259
277
|
}
|
|
260
278
|
}
|
|
261
279
|
function sessionArtifactDir(artifactRoot, sessionId) {
|
|
@@ -288,7 +306,11 @@ function planEnv(planRoot, projectId, branchName, activePlanId) {
|
|
|
288
306
|
return env;
|
|
289
307
|
}
|
|
290
308
|
function isArtifactEnvPath(filePath) {
|
|
291
|
-
|
|
309
|
+
try {
|
|
310
|
+
return parseBuiltInToolPath(filePath)?.ref === R5D_ARTIFACTS_DIR_REF;
|
|
311
|
+
} catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
292
314
|
}
|
|
293
315
|
function artifactApiBasePath(projectId, branchName, sessionId) {
|
|
294
316
|
return `/preview-api/${encodeURIComponent(projectId)}/${encodeURIComponent(branchName)}/artifacts/${encodeURIComponent(sessionId)}`;
|
|
@@ -422,40 +444,45 @@ async function syncSessionArtifacts(input) {
|
|
|
422
444
|
void queued.then(release, release);
|
|
423
445
|
return queued;
|
|
424
446
|
}
|
|
425
|
-
async function
|
|
426
|
-
|
|
427
|
-
|
|
447
|
+
async function prepareBuiltInToolPaths(input) {
|
|
448
|
+
const parsed = parseBuiltInToolPath(input.filePath);
|
|
449
|
+
if (!parsed) {
|
|
450
|
+
return void 0;
|
|
428
451
|
}
|
|
429
|
-
if (
|
|
430
|
-
|
|
452
|
+
if (parsed.ref === R5D_ARTIFACTS_DIR_REF) {
|
|
453
|
+
if (input.access === "write") {
|
|
454
|
+
throw new Error(
|
|
455
|
+
`${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
if (!input.sessionId) {
|
|
459
|
+
throw new Error(`${R5D_ARTIFACTS_DIR_REF} paths require an active chat session`);
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
artifactsDir: await syncSessionArtifacts({
|
|
463
|
+
baseUrl: input.baseUrl,
|
|
464
|
+
token: input.token,
|
|
465
|
+
projectId: input.projectId,
|
|
466
|
+
branchName: input.branchName,
|
|
467
|
+
sessionId: input.sessionId,
|
|
468
|
+
artifactRoot: input.artifactRoot
|
|
469
|
+
})
|
|
470
|
+
};
|
|
431
471
|
}
|
|
432
|
-
|
|
433
|
-
|
|
472
|
+
const plansDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
|
|
473
|
+
fs.mkdirSync(plansDir, { recursive: true });
|
|
474
|
+
if (parsed.ref === R5D_PLANS_DIR_REF) {
|
|
475
|
+
return { plansDir };
|
|
434
476
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
token: input.token,
|
|
438
|
-
projectId: input.projectId,
|
|
439
|
-
branchName: input.branchName,
|
|
440
|
-
sessionId: input.sessionId,
|
|
441
|
-
artifactRoot: input.artifactRoot
|
|
442
|
-
});
|
|
443
|
-
const relativePath = path.posix.normalize(input.filePath.slice(`${R5D_ARTIFACTS_DIR_REF}/`.length));
|
|
444
|
-
if (!relativePath || relativePath === "." || relativePath.startsWith("../") || relativePath.includes("\0")) {
|
|
445
|
-
throw new Error(`Invalid artifact path: ${input.filePath}`);
|
|
477
|
+
if (!input.activePlanId) {
|
|
478
|
+
throw new Error(`${R5D_ACTIVE_PLAN_FILE_REF} paths require an active plan`);
|
|
446
479
|
}
|
|
447
|
-
|
|
448
|
-
assertInsideRoot(targetDir, absolutePath, "Artifact path");
|
|
480
|
+
validatePlanId(input.activePlanId);
|
|
449
481
|
return {
|
|
450
|
-
|
|
451
|
-
|
|
482
|
+
plansDir,
|
|
483
|
+
activePlanFile: path.join(plansDir, `${input.activePlanId}.plan.md`)
|
|
452
484
|
};
|
|
453
485
|
}
|
|
454
|
-
function rejectArtifactWritePath(filePath) {
|
|
455
|
-
if (isArtifactEnvPath(filePath)) {
|
|
456
|
-
throw new Error(`${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
486
|
async function prepareArtifactEnvForShell(input) {
|
|
460
487
|
if (!input.sessionId) {
|
|
461
488
|
return {};
|
|
@@ -606,10 +633,63 @@ function assertAllowedProjectPath(repoRelativePath, inputPath) {
|
|
|
606
633
|
throw new Error(`Invalid project file path: ${inputPath}`);
|
|
607
634
|
}
|
|
608
635
|
}
|
|
609
|
-
function
|
|
636
|
+
function assertPathInsideRealRoot(rootPath, candidatePath, label) {
|
|
637
|
+
const resolvedRoot = path.resolve(rootPath);
|
|
638
|
+
const resolvedCandidate = path.resolve(candidatePath);
|
|
639
|
+
assertInsideRoot(resolvedRoot, resolvedCandidate, label);
|
|
640
|
+
let existingPath = resolvedCandidate;
|
|
641
|
+
while (!fs.existsSync(existingPath)) {
|
|
642
|
+
const parentPath = path.dirname(existingPath);
|
|
643
|
+
if (parentPath === existingPath) {
|
|
644
|
+
throw new Error(`${label} root does not exist: ${rootPath}`);
|
|
645
|
+
}
|
|
646
|
+
existingPath = parentPath;
|
|
647
|
+
}
|
|
648
|
+
const realRoot = fs.realpathSync(resolvedRoot);
|
|
649
|
+
const realExistingPath = fs.realpathSync(existingPath);
|
|
650
|
+
assertInsideRoot(realRoot, realExistingPath, label);
|
|
651
|
+
}
|
|
652
|
+
function resolveVirtualWorkerFilePath(inputPath, parsed, builtInPaths) {
|
|
653
|
+
if (parsed.relativePath.split("/").includes("..")) {
|
|
654
|
+
throw new Error(`Invalid ${parsed.ref} path: ${inputPath}`);
|
|
655
|
+
}
|
|
656
|
+
let rootPath;
|
|
657
|
+
let absolutePath;
|
|
658
|
+
if (parsed.ref === R5D_ARTIFACTS_DIR_REF) {
|
|
659
|
+
rootPath = builtInPaths?.artifactsDir;
|
|
660
|
+
} else if (parsed.ref === R5D_PLANS_DIR_REF) {
|
|
661
|
+
rootPath = builtInPaths?.plansDir;
|
|
662
|
+
} else {
|
|
663
|
+
if (parsed.relativePath) {
|
|
664
|
+
throw new Error(`${R5D_ACTIVE_PLAN_FILE_REF} must reference the active plan file directly`);
|
|
665
|
+
}
|
|
666
|
+
rootPath = builtInPaths?.plansDir;
|
|
667
|
+
absolutePath = builtInPaths?.activePlanFile;
|
|
668
|
+
}
|
|
669
|
+
if (!rootPath || parsed.ref === R5D_ACTIVE_PLAN_FILE_REF && !absolutePath) {
|
|
670
|
+
const requirement = parsed.ref === R5D_ARTIFACTS_DIR_REF ? "an active chat session" : "an active plan";
|
|
671
|
+
throw new Error(`${parsed.ref} paths require ${requirement}`);
|
|
672
|
+
}
|
|
673
|
+
const normalizedPath = parsed.relativePath ? path.posix.normalize(parsed.relativePath).replace(/^\/+|\/+$/g, "") : "";
|
|
674
|
+
const normalizedRelativePath = normalizedPath === "." ? "" : normalizedPath;
|
|
675
|
+
absolutePath ??= normalizedRelativePath ? path.resolve(rootPath, ...normalizedRelativePath.split("/")) : path.resolve(rootPath);
|
|
676
|
+
assertPathInsideRealRoot(rootPath, absolutePath, `${parsed.ref} path`);
|
|
677
|
+
return {
|
|
678
|
+
absolutePath,
|
|
679
|
+
displayPath: normalizedRelativePath ? `${parsed.ref}/${normalizedRelativePath}` : parsed.ref,
|
|
680
|
+
repoRelativePath: null,
|
|
681
|
+
scope: "virtual",
|
|
682
|
+
virtualRootPath: path.resolve(rootPath)
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function resolveWorkerFilePath(branchPath, inputPath, builtInPaths) {
|
|
610
686
|
if (typeof inputPath !== "string" || inputPath.length === 0 || inputPath.includes("\0")) {
|
|
611
687
|
throw new Error(`File path must be a non-empty string. Got: ${JSON.stringify(inputPath)}`);
|
|
612
688
|
}
|
|
689
|
+
const parsedBuiltInPath = parseBuiltInToolPath(inputPath);
|
|
690
|
+
if (parsedBuiltInPath) {
|
|
691
|
+
return resolveVirtualWorkerFilePath(inputPath, parsedBuiltInPath, builtInPaths);
|
|
692
|
+
}
|
|
613
693
|
const resolvedBranchPath = path.resolve(branchPath);
|
|
614
694
|
if (path.isAbsolute(inputPath)) {
|
|
615
695
|
const absolutePath2 = path.resolve(inputPath);
|
|
@@ -1066,13 +1146,13 @@ async function withFileMutationQueue(key, operation) {
|
|
|
1066
1146
|
}
|
|
1067
1147
|
}
|
|
1068
1148
|
function mutationQueueKey(projectId, branchName, resolved) {
|
|
1069
|
-
if (resolved.scope
|
|
1070
|
-
return
|
|
1149
|
+
if (resolved.scope !== "project") {
|
|
1150
|
+
return `${resolved.scope}:${path.normalize(resolved.absolutePath)}`;
|
|
1071
1151
|
}
|
|
1072
1152
|
return `project:${projectId}:${branchName}:${path.posix.normalize(resolved.repoRelativePath)}`;
|
|
1073
1153
|
}
|
|
1074
|
-
function readWorkerTextFile(branchPath, filePath, offset, limit) {
|
|
1075
|
-
const resolved = resolveWorkerFilePath(branchPath, filePath);
|
|
1154
|
+
function readWorkerTextFile(branchPath, filePath, offset, limit, builtInPaths) {
|
|
1155
|
+
const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
|
|
1076
1156
|
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
|
|
1077
1157
|
throw new Error(`File not found: ${resolved.displayPath}`);
|
|
1078
1158
|
}
|
|
@@ -1086,8 +1166,8 @@ function readWorkerTextFile(branchPath, filePath, offset, limit) {
|
|
|
1086
1166
|
...formatted
|
|
1087
1167
|
};
|
|
1088
1168
|
}
|
|
1089
|
-
function writeWorkerTextFile(branchPath, filePath, content) {
|
|
1090
|
-
const resolved = resolveWorkerFilePath(branchPath, filePath);
|
|
1169
|
+
function writeWorkerTextFile(branchPath, filePath, content, builtInPaths) {
|
|
1170
|
+
const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
|
|
1091
1171
|
fs.mkdirSync(path.dirname(resolved.absolutePath), { recursive: true });
|
|
1092
1172
|
fs.writeFileSync(resolved.absolutePath, content, "utf8");
|
|
1093
1173
|
return {
|
|
@@ -1096,8 +1176,8 @@ function writeWorkerTextFile(branchPath, filePath, content) {
|
|
|
1096
1176
|
gitBlobHash: getGitBlobHashForContent(content)
|
|
1097
1177
|
};
|
|
1098
1178
|
}
|
|
1099
|
-
function editWorkerTextFile(branchPath, filePath, edits) {
|
|
1100
|
-
const resolved = resolveWorkerFilePath(branchPath, filePath);
|
|
1179
|
+
function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
|
|
1180
|
+
const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
|
|
1101
1181
|
if (!Array.isArray(edits) || edits.length === 0) {
|
|
1102
1182
|
throw new Error("edit requires at least one replacement");
|
|
1103
1183
|
}
|
|
@@ -1144,46 +1224,57 @@ function editWorkerTextFile(branchPath, filePath, edits) {
|
|
|
1144
1224
|
};
|
|
1145
1225
|
}
|
|
1146
1226
|
async function executeReadFileOperation(input) {
|
|
1147
|
-
const
|
|
1227
|
+
const builtInPaths = await prepareBuiltInToolPaths({
|
|
1148
1228
|
filePath: input.message.filePath,
|
|
1149
1229
|
baseUrl: input.baseUrl,
|
|
1150
1230
|
token: input.token,
|
|
1151
1231
|
projectId: input.projectId,
|
|
1152
1232
|
branchName: input.message.branchName,
|
|
1153
1233
|
sessionId: input.message.sessionId,
|
|
1154
|
-
|
|
1234
|
+
activePlanId: input.message.activePlanId,
|
|
1235
|
+
artifactRoot: input.artifactRoot,
|
|
1236
|
+
planRoot: input.planRoot,
|
|
1237
|
+
access: "read"
|
|
1155
1238
|
});
|
|
1156
|
-
if (artifact) {
|
|
1157
|
-
if (!fs.existsSync(artifact.absolutePath) || !fs.statSync(artifact.absolutePath).isFile()) {
|
|
1158
|
-
throw new Error(`File not found: ${artifact.virtualPath}`);
|
|
1159
|
-
}
|
|
1160
|
-
const buffer = fs.readFileSync(artifact.absolutePath);
|
|
1161
|
-
const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
|
|
1162
|
-
return {
|
|
1163
|
-
type: "read",
|
|
1164
|
-
kind: "text",
|
|
1165
|
-
file: artifact.virtualPath,
|
|
1166
|
-
gitBlobHash: getGitBlobHashForContent(buffer),
|
|
1167
|
-
...formatted
|
|
1168
|
-
};
|
|
1169
|
-
}
|
|
1170
1239
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1171
|
-
return readWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.offset, input.message.limit);
|
|
1240
|
+
return readWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.offset, input.message.limit, builtInPaths);
|
|
1172
1241
|
}
|
|
1173
1242
|
async function executeWriteFileOperation(input) {
|
|
1174
|
-
|
|
1243
|
+
const builtInPaths = await prepareBuiltInToolPaths({
|
|
1244
|
+
filePath: input.message.filePath,
|
|
1245
|
+
baseUrl: input.baseUrl,
|
|
1246
|
+
token: input.token,
|
|
1247
|
+
projectId: input.projectId,
|
|
1248
|
+
branchName: input.message.branchName,
|
|
1249
|
+
sessionId: input.message.sessionId,
|
|
1250
|
+
activePlanId: input.message.activePlanId,
|
|
1251
|
+
artifactRoot: input.artifactRoot,
|
|
1252
|
+
planRoot: input.planRoot,
|
|
1253
|
+
access: "write"
|
|
1254
|
+
});
|
|
1175
1255
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1176
|
-
const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath);
|
|
1256
|
+
const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath, builtInPaths);
|
|
1177
1257
|
return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
|
|
1178
|
-
return writeWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.content);
|
|
1258
|
+
return writeWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.content, builtInPaths);
|
|
1179
1259
|
});
|
|
1180
1260
|
}
|
|
1181
1261
|
async function executeEditFileOperation(input) {
|
|
1182
|
-
|
|
1262
|
+
const builtInPaths = await prepareBuiltInToolPaths({
|
|
1263
|
+
filePath: input.message.filePath,
|
|
1264
|
+
baseUrl: input.baseUrl,
|
|
1265
|
+
token: input.token,
|
|
1266
|
+
projectId: input.projectId,
|
|
1267
|
+
branchName: input.message.branchName,
|
|
1268
|
+
sessionId: input.message.sessionId,
|
|
1269
|
+
activePlanId: input.message.activePlanId,
|
|
1270
|
+
artifactRoot: input.artifactRoot,
|
|
1271
|
+
planRoot: input.planRoot,
|
|
1272
|
+
access: "write"
|
|
1273
|
+
});
|
|
1183
1274
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1184
|
-
const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath);
|
|
1275
|
+
const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath, builtInPaths);
|
|
1185
1276
|
return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
|
|
1186
|
-
return editWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.edits);
|
|
1277
|
+
return editWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.edits, builtInPaths);
|
|
1187
1278
|
});
|
|
1188
1279
|
}
|
|
1189
1280
|
const IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".cache", "coverage", "target"]);
|
|
@@ -1204,16 +1295,20 @@ function normalizeFindPattern(pattern) {
|
|
|
1204
1295
|
function walkWorkerEntries(branchPath, start) {
|
|
1205
1296
|
const entries = [];
|
|
1206
1297
|
const visitedDirectories = /* @__PURE__ */ new Set();
|
|
1298
|
+
const realVirtualRoot = start.scope === "virtual" ? fs.realpathSync(start.virtualRootPath) : null;
|
|
1207
1299
|
const visit = (absolutePath, isRoot) => {
|
|
1208
1300
|
let stat;
|
|
1209
1301
|
try {
|
|
1210
1302
|
stat = fs.statSync(absolutePath);
|
|
1303
|
+
if (realVirtualRoot) {
|
|
1304
|
+
assertInsideRoot(realVirtualRoot, fs.realpathSync(absolutePath), `${start.displayPath} path`);
|
|
1305
|
+
}
|
|
1211
1306
|
} catch (error) {
|
|
1212
1307
|
if (isRoot) throw error;
|
|
1213
1308
|
return;
|
|
1214
1309
|
}
|
|
1215
|
-
const displayPath = start.scope === "project" ? toProjectDisplayPath(branchPath, absolutePath) : path.resolve(absolutePath);
|
|
1216
1310
|
const hostRelativePath = path.relative(start.absolutePath, absolutePath).split(path.sep).join(path.posix.sep);
|
|
1311
|
+
const displayPath = start.scope === "project" ? toProjectDisplayPath(branchPath, absolutePath) : start.scope === "virtual" ? hostRelativePath ? `${start.displayPath}/${hostRelativePath}` : start.displayPath : path.resolve(absolutePath);
|
|
1217
1312
|
const matchPath = start.scope === "project" ? displayPath : hostRelativePath || path.basename(start.absolutePath);
|
|
1218
1313
|
entries.push({ absolutePath, displayPath, matchPath, stat });
|
|
1219
1314
|
if (!stat.isDirectory()) return;
|
|
@@ -1244,8 +1339,8 @@ function walkWorkerEntries(branchPath, start) {
|
|
|
1244
1339
|
function isProbablyText(bytes) {
|
|
1245
1340
|
return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
|
|
1246
1341
|
}
|
|
1247
|
-
function grepWorkerFiles(branchPath, input) {
|
|
1248
|
-
const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
|
|
1342
|
+
function grepWorkerFiles(branchPath, input, builtInPaths) {
|
|
1343
|
+
const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
|
|
1249
1344
|
if (!fs.existsSync(start.absolutePath)) {
|
|
1250
1345
|
throw new Error(`Path not found: ${start.displayPath}`);
|
|
1251
1346
|
}
|
|
@@ -1291,17 +1386,34 @@ function grepWorkerFiles(branchPath, input) {
|
|
|
1291
1386
|
};
|
|
1292
1387
|
}
|
|
1293
1388
|
async function executeGrepOperation(input) {
|
|
1294
|
-
const
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1389
|
+
const toolPath = input.message.path ?? ".";
|
|
1390
|
+
const builtInPaths = await prepareBuiltInToolPaths({
|
|
1391
|
+
filePath: toolPath,
|
|
1392
|
+
baseUrl: input.baseUrl,
|
|
1393
|
+
token: input.token,
|
|
1394
|
+
projectId: input.projectId,
|
|
1395
|
+
branchName: input.message.branchName,
|
|
1396
|
+
sessionId: input.message.sessionId,
|
|
1397
|
+
activePlanId: input.message.activePlanId,
|
|
1398
|
+
artifactRoot: input.artifactRoot,
|
|
1399
|
+
planRoot: input.planRoot,
|
|
1400
|
+
access: "read"
|
|
1301
1401
|
});
|
|
1402
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1403
|
+
return grepWorkerFiles(
|
|
1404
|
+
workspace.branchPath,
|
|
1405
|
+
{
|
|
1406
|
+
pattern: input.message.pattern,
|
|
1407
|
+
path: input.message.path,
|
|
1408
|
+
glob: input.message.glob,
|
|
1409
|
+
caseSensitive: input.message.caseSensitive,
|
|
1410
|
+
limit: input.message.limit
|
|
1411
|
+
},
|
|
1412
|
+
builtInPaths
|
|
1413
|
+
);
|
|
1302
1414
|
}
|
|
1303
|
-
function findWorkerFiles(branchPath, input) {
|
|
1304
|
-
const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
|
|
1415
|
+
function findWorkerFiles(branchPath, input, builtInPaths) {
|
|
1416
|
+
const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
|
|
1305
1417
|
if (!fs.existsSync(start.absolutePath) || !fs.statSync(start.absolutePath).isDirectory()) {
|
|
1306
1418
|
throw new Error(`Directory not found: ${start.displayPath}`);
|
|
1307
1419
|
}
|
|
@@ -1335,16 +1447,33 @@ function findWorkerFiles(branchPath, input) {
|
|
|
1335
1447
|
};
|
|
1336
1448
|
}
|
|
1337
1449
|
async function executeFindOperation(input) {
|
|
1338
|
-
const
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1450
|
+
const toolPath = input.message.path ?? ".";
|
|
1451
|
+
const builtInPaths = await prepareBuiltInToolPaths({
|
|
1452
|
+
filePath: toolPath,
|
|
1453
|
+
baseUrl: input.baseUrl,
|
|
1454
|
+
token: input.token,
|
|
1455
|
+
projectId: input.projectId,
|
|
1456
|
+
branchName: input.message.branchName,
|
|
1457
|
+
sessionId: input.message.sessionId,
|
|
1458
|
+
activePlanId: input.message.activePlanId,
|
|
1459
|
+
artifactRoot: input.artifactRoot,
|
|
1460
|
+
planRoot: input.planRoot,
|
|
1461
|
+
access: "read"
|
|
1344
1462
|
});
|
|
1463
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1464
|
+
return findWorkerFiles(
|
|
1465
|
+
workspace.branchPath,
|
|
1466
|
+
{
|
|
1467
|
+
pattern: input.message.pattern,
|
|
1468
|
+
path: input.message.path,
|
|
1469
|
+
entryType: input.message.entryType,
|
|
1470
|
+
limit: input.message.limit
|
|
1471
|
+
},
|
|
1472
|
+
builtInPaths
|
|
1473
|
+
);
|
|
1345
1474
|
}
|
|
1346
|
-
function listWorkerDirectory(branchPath, inputPath = ".", inputLimit) {
|
|
1347
|
-
const resolved = resolveWorkerFilePath(branchPath, inputPath);
|
|
1475
|
+
function listWorkerDirectory(branchPath, inputPath = ".", inputLimit, builtInPaths) {
|
|
1476
|
+
const resolved = resolveWorkerFilePath(branchPath, inputPath, builtInPaths);
|
|
1348
1477
|
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isDirectory()) {
|
|
1349
1478
|
throw new Error(`Directory not found: ${resolved.displayPath}`);
|
|
1350
1479
|
}
|
|
@@ -1362,11 +1491,24 @@ function listWorkerDirectory(branchPath, inputPath = ".", inputLimit) {
|
|
|
1362
1491
|
};
|
|
1363
1492
|
}
|
|
1364
1493
|
async function executeLsOperation(input) {
|
|
1494
|
+
const toolPath = input.message.path ?? ".";
|
|
1495
|
+
const builtInPaths = await prepareBuiltInToolPaths({
|
|
1496
|
+
filePath: toolPath,
|
|
1497
|
+
baseUrl: input.baseUrl,
|
|
1498
|
+
token: input.token,
|
|
1499
|
+
projectId: input.projectId,
|
|
1500
|
+
branchName: input.message.branchName,
|
|
1501
|
+
sessionId: input.message.sessionId,
|
|
1502
|
+
activePlanId: input.message.activePlanId,
|
|
1503
|
+
artifactRoot: input.artifactRoot,
|
|
1504
|
+
planRoot: input.planRoot,
|
|
1505
|
+
access: "read"
|
|
1506
|
+
});
|
|
1365
1507
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1366
|
-
return listWorkerDirectory(workspace.branchPath, input.message.path, input.message.limit);
|
|
1508
|
+
return listWorkerDirectory(workspace.branchPath, input.message.path, input.message.limit, builtInPaths);
|
|
1367
1509
|
}
|
|
1368
|
-
function readWorkerImageFile(branchPath, filePath) {
|
|
1369
|
-
const resolved = resolveWorkerFilePath(branchPath, filePath);
|
|
1510
|
+
function readWorkerImageFile(branchPath, filePath, builtInPaths) {
|
|
1511
|
+
const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
|
|
1370
1512
|
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
|
|
1371
1513
|
throw new Error(`File not found: ${resolved.displayPath}`);
|
|
1372
1514
|
}
|
|
@@ -1388,38 +1530,20 @@ function readWorkerImageFile(branchPath, filePath) {
|
|
|
1388
1530
|
};
|
|
1389
1531
|
}
|
|
1390
1532
|
async function executeViewFileBytesOperation(input) {
|
|
1391
|
-
const
|
|
1533
|
+
const builtInPaths = await prepareBuiltInToolPaths({
|
|
1392
1534
|
filePath: input.message.filePath,
|
|
1393
1535
|
baseUrl: input.baseUrl,
|
|
1394
1536
|
token: input.token,
|
|
1395
1537
|
projectId: input.projectId,
|
|
1396
1538
|
branchName: input.message.branchName,
|
|
1397
1539
|
sessionId: input.message.sessionId,
|
|
1398
|
-
|
|
1540
|
+
activePlanId: input.message.activePlanId,
|
|
1541
|
+
artifactRoot: input.artifactRoot,
|
|
1542
|
+
planRoot: input.planRoot,
|
|
1543
|
+
access: "read"
|
|
1399
1544
|
});
|
|
1400
|
-
if (artifact) {
|
|
1401
|
-
if (!fs.existsSync(artifact.absolutePath) || !fs.statSync(artifact.absolutePath).isFile()) {
|
|
1402
|
-
throw new Error(`File not found: ${artifact.virtualPath}`);
|
|
1403
|
-
}
|
|
1404
|
-
const extension = path.extname(artifact.absolutePath).toLowerCase();
|
|
1405
|
-
const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
|
|
1406
|
-
if (!mediaType) {
|
|
1407
|
-
throw new Error("read supports PNG and JPEG image inputs only");
|
|
1408
|
-
}
|
|
1409
|
-
const bytes = fs.readFileSync(artifact.absolutePath);
|
|
1410
|
-
const dimensions = readImageDimensions(bytes, mediaType);
|
|
1411
|
-
return {
|
|
1412
|
-
type: "view_file_bytes",
|
|
1413
|
-
filePath: artifact.virtualPath,
|
|
1414
|
-
mediaType,
|
|
1415
|
-
base64: bytes.toString("base64"),
|
|
1416
|
-
fileSizeBytes: bytes.length,
|
|
1417
|
-
width: dimensions.width,
|
|
1418
|
-
height: dimensions.height
|
|
1419
|
-
};
|
|
1420
|
-
}
|
|
1421
1545
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1422
|
-
return readWorkerImageFile(workspace.branchPath, input.message.filePath);
|
|
1546
|
+
return readWorkerImageFile(workspace.branchPath, input.message.filePath, builtInPaths);
|
|
1423
1547
|
}
|
|
1424
1548
|
async function executeOperation(input) {
|
|
1425
1549
|
switch (input.message.type) {
|
|
@@ -2058,6 +2182,7 @@ async function startWorker(options) {
|
|
|
2058
2182
|
fs.mkdirSync(artifactRoot, { recursive: true });
|
|
2059
2183
|
fs.mkdirSync(planRoot, { recursive: true });
|
|
2060
2184
|
const manifestByProjectId = /* @__PURE__ */ new Map();
|
|
2185
|
+
const pendingManifestCheckouts = /* @__PURE__ */ new Map();
|
|
2061
2186
|
const workspaceSyncSingleFlight = new WorkspaceSyncSingleFlight();
|
|
2062
2187
|
let workspaceRemoteUrl = null;
|
|
2063
2188
|
let activeWorkspaceIncidentId = null;
|
|
@@ -2086,37 +2211,64 @@ async function startWorker(options) {
|
|
|
2086
2211
|
const sendWorkspaceSyncResult = (requestId, result) => {
|
|
2087
2212
|
sendWorkerMessage(ws, { type: "workspace_sync_result", requestId, result });
|
|
2088
2213
|
};
|
|
2089
|
-
const
|
|
2214
|
+
const manifestCheckoutKey = (projectId, branchName) => `${projectId}\0${branchName}`;
|
|
2215
|
+
const allManifestCheckouts = () => [...manifestByProjectId.values()].flatMap(
|
|
2216
|
+
(manifest) => manifest.branches.map((branchName) => ({ projectId: manifest.projectId, branchName }))
|
|
2217
|
+
);
|
|
2218
|
+
const ensureVisibleWorkspaceCheckouts = (targets) => {
|
|
2090
2219
|
const created = [];
|
|
2091
|
-
for (const
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
projectRoot: projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId),
|
|
2098
|
-
branchName,
|
|
2099
|
-
githubRemoteUrl,
|
|
2100
|
-
githubAuth: gitAuthForRemote(githubRemoteUrl, manifest.repoAuthHeader),
|
|
2101
|
-
githubAuthHeader: manifest.repoAuthHeader,
|
|
2102
|
-
defaultBranch: manifest.defaultBranch || "main",
|
|
2103
|
-
reconcileExistingOrigin: Boolean(manifest.repoHttpUrl)
|
|
2104
|
-
});
|
|
2105
|
-
if (!existed) created.push({ projectId: manifest.projectId, branchName });
|
|
2220
|
+
for (const target of targets) {
|
|
2221
|
+
const manifest = manifestByProjectId.get(target.projectId);
|
|
2222
|
+
const key = manifestCheckoutKey(target.projectId, target.branchName);
|
|
2223
|
+
if (!manifest || !manifest.branches.includes(target.branchName)) {
|
|
2224
|
+
pendingManifestCheckouts.delete(key);
|
|
2225
|
+
continue;
|
|
2106
2226
|
}
|
|
2227
|
+
const projectRoot = projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId);
|
|
2228
|
+
const branchPath = path.join(projectRoot, target.branchName);
|
|
2229
|
+
const existed = hasNormalVisibleGitDir(branchPath);
|
|
2230
|
+
const githubRemoteUrl = githubRemoteUrlFor(baseUrl, manifest.projectId, manifest);
|
|
2231
|
+
ensureVisibleGitCheckout({
|
|
2232
|
+
projectRoot,
|
|
2233
|
+
branchName: target.branchName,
|
|
2234
|
+
githubRemoteUrl,
|
|
2235
|
+
githubAuth: gitAuthForRemote(githubRemoteUrl, manifest.repoAuthHeader),
|
|
2236
|
+
githubAuthHeader: manifest.repoAuthHeader,
|
|
2237
|
+
defaultBranch: manifest.defaultBranch || "main",
|
|
2238
|
+
reconcileExistingOrigin: Boolean(manifest.repoHttpUrl)
|
|
2239
|
+
});
|
|
2240
|
+
if (!existed) created.push(target);
|
|
2107
2241
|
}
|
|
2108
2242
|
return created;
|
|
2109
2243
|
};
|
|
2110
2244
|
const runRequestedWorkspaceSync = async (requestId, trigger, overrides = {}) => {
|
|
2111
2245
|
workspaceSyncRequestsInFlight += 1;
|
|
2112
2246
|
try {
|
|
2113
|
-
|
|
2114
|
-
const result = await workspaceSyncSingleFlight.
|
|
2115
|
-
|
|
2247
|
+
let pendingTargets = [];
|
|
2248
|
+
const result = await workspaceSyncSingleFlight.runPrepared(() => {
|
|
2249
|
+
const shouldEnsureCheckouts = !overrides.skipVisibleMirror && !overrides.resetToCanonical;
|
|
2250
|
+
let checkoutTargets = [];
|
|
2251
|
+
if (shouldEnsureCheckouts) {
|
|
2252
|
+
checkoutTargets = trigger.type === "connect" ? allManifestCheckouts() : [...pendingManifestCheckouts.values()];
|
|
2253
|
+
}
|
|
2254
|
+
pendingTargets = shouldEnsureCheckouts ? [...pendingManifestCheckouts.values()] : [];
|
|
2255
|
+
const createdCheckouts = ensureVisibleWorkspaceCheckouts(checkoutTargets);
|
|
2256
|
+
const newVisibleCheckoutByKey = /* @__PURE__ */ new Map();
|
|
2257
|
+
for (const target of [...overrides.newVisibleCheckouts ?? [], ...createdCheckouts, ...pendingTargets]) {
|
|
2258
|
+
newVisibleCheckoutByKey.set(manifestCheckoutKey(target.projectId, target.branchName), target);
|
|
2259
|
+
}
|
|
2260
|
+
const newVisibleCheckouts = [...newVisibleCheckoutByKey.values()];
|
|
2261
|
+
return workspaceSyncInput(trigger, {
|
|
2116
2262
|
...overrides,
|
|
2117
2263
|
...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
|
|
2118
|
-
})
|
|
2119
|
-
);
|
|
2264
|
+
});
|
|
2265
|
+
});
|
|
2266
|
+
if (result.outcome === "no_change" || result.outcome === "published" || result.outcome === "updated" || result.outcome === "conflict_reset") {
|
|
2267
|
+
for (const target of pendingTargets) {
|
|
2268
|
+
const key = manifestCheckoutKey(target.projectId, target.branchName);
|
|
2269
|
+
if (pendingManifestCheckouts.get(key) === target) pendingManifestCheckouts.delete(key);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2120
2272
|
previousPeriodicFingerprint = null;
|
|
2121
2273
|
sendWorkspaceSyncResult(requestId, result);
|
|
2122
2274
|
return result;
|
|
@@ -2209,9 +2361,26 @@ async function startWorker(options) {
|
|
|
2209
2361
|
configureGitHubAuth(message.githubCredential);
|
|
2210
2362
|
visibleGitIdentity = message.gitIdentity;
|
|
2211
2363
|
workspaceRemoteUrl = message.workspaceRemoteUrl;
|
|
2364
|
+
const previousCheckoutKeys = new Set(
|
|
2365
|
+
allManifestCheckouts().map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
|
|
2366
|
+
);
|
|
2212
2367
|
const migratedProjectIds = migrateLegacyProjectRoots(projectsRoot, message.projects);
|
|
2213
2368
|
manifestByProjectId.clear();
|
|
2214
2369
|
for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
|
|
2370
|
+
const currentCheckouts = allManifestCheckouts();
|
|
2371
|
+
const currentCheckoutKeys = new Set(
|
|
2372
|
+
currentCheckouts.map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
|
|
2373
|
+
);
|
|
2374
|
+
for (const key of pendingManifestCheckouts.keys()) {
|
|
2375
|
+
if (!currentCheckoutKeys.has(key)) pendingManifestCheckouts.delete(key);
|
|
2376
|
+
}
|
|
2377
|
+
for (const checkout of currentCheckouts) {
|
|
2378
|
+
const key = manifestCheckoutKey(checkout.projectId, checkout.branchName);
|
|
2379
|
+
if (previousCheckoutKeys.has(key)) continue;
|
|
2380
|
+
const branchPath = path.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
|
|
2381
|
+
if (!hasNormalVisibleGitDir(branchPath)) pendingManifestCheckouts.set(key, checkout);
|
|
2382
|
+
}
|
|
2383
|
+
previousPeriodicFingerprint = null;
|
|
2215
2384
|
process.stdout.write(
|
|
2216
2385
|
`[r5d-worker] workspace manifest: ${message.projects.length} projects${migratedProjectIds.length > 0 ? `; migrated ${migratedProjectIds.length} project checkout root(s)` : ""}
|
|
2217
2386
|
`
|
|
@@ -2222,8 +2391,15 @@ async function startWorker(options) {
|
|
|
2222
2391
|
if (!workspaceRemoteUrl || activeWorkspaceIncidentId || cliUpdateInProgress || periodicWorkspaceScanInFlight) return;
|
|
2223
2392
|
periodicWorkspaceScanInFlight = true;
|
|
2224
2393
|
void (async () => {
|
|
2225
|
-
|
|
2226
|
-
|
|
2394
|
+
if (pendingManifestCheckouts.size > 0) {
|
|
2395
|
+
previousPeriodicFingerprint = null;
|
|
2396
|
+
await runRequestedWorkspaceSync(crypto.randomUUID(), {
|
|
2397
|
+
type: "periodic",
|
|
2398
|
+
detail: "workspace manifest additions"
|
|
2399
|
+
});
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
const fingerprint = await workspaceSyncSingleFlight.fingerprintPrepared(() => workspaceSyncInput({ type: "periodic" }));
|
|
2227
2403
|
if (fingerprint === emptyFingerprint) {
|
|
2228
2404
|
previousPeriodicFingerprint = null;
|
|
2229
2405
|
return;
|
|
@@ -2490,6 +2666,7 @@ async function startWorker(options) {
|
|
|
2490
2666
|
projectRoot,
|
|
2491
2667
|
syncRoot,
|
|
2492
2668
|
artifactRoot,
|
|
2669
|
+
planRoot,
|
|
2493
2670
|
manifest
|
|
2494
2671
|
});
|
|
2495
2672
|
ws.send(
|
package/dist/mjs/package.json
CHANGED