@ricsam/r5d-worker 0.0.29 → 0.0.31
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 +252 -123
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/process-tree.cjs +117 -0
- package/dist/mjs/main.mjs +246 -123
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/process-tree.mjs +93 -0
- package/dist/types/main.d.ts +68 -2
- package/dist/types/process-tree.d.ts +16 -0
- package/package.json +1 -1
package/dist/mjs/main.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { spawn as spawnChildProcess } from "node:child_process";
|
|
|
7
7
|
import { installCliUpdate, readInstalledCliVersion } from "./cli-update.mjs";
|
|
8
8
|
import { configureVisibleGitIdentity } from "./git-identity.mjs";
|
|
9
9
|
import { hasWorkerHeartbeatTimedOut, WORKER_HEARTBEAT_INTERVAL_MS } from "./heartbeat.mjs";
|
|
10
|
+
import { terminateProcessTree } from "./process-tree.mjs";
|
|
10
11
|
import { superviseWorkerRuntime, WORKER_RELOAD_EXIT_CODE, WORKER_RUNTIME_ENV } from "./supervisor.mjs";
|
|
11
12
|
const DEFAULT_BASE_URL = "https://r5d.dev";
|
|
12
13
|
const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
|
|
@@ -662,36 +663,69 @@ function isInsideBranchPath(branchPath, filePath) {
|
|
|
662
663
|
const relative = path.relative(path.resolve(branchPath), path.resolve(filePath));
|
|
663
664
|
return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
664
665
|
}
|
|
665
|
-
function
|
|
666
|
-
|
|
667
|
-
|
|
666
|
+
function toProjectDisplayPath(branchPath, absolutePath) {
|
|
667
|
+
const relative = path.relative(path.resolve(branchPath), path.resolve(absolutePath)).split(path.sep).join(path.posix.sep);
|
|
668
|
+
return relative || ".";
|
|
669
|
+
}
|
|
670
|
+
function assertAllowedProjectPath(repoRelativePath, inputPath) {
|
|
671
|
+
if (repoRelativePath === ".git" || repoRelativePath.startsWith(".git/")) {
|
|
672
|
+
throw new Error("Refusing to access .git through worker file tools");
|
|
673
|
+
}
|
|
674
|
+
if (repoRelativePath.split("/").includes("..")) {
|
|
675
|
+
throw new Error(`Invalid project file path: ${inputPath}`);
|
|
668
676
|
}
|
|
669
677
|
}
|
|
670
|
-
function
|
|
671
|
-
if (typeof
|
|
672
|
-
throw new Error(`File path must be
|
|
678
|
+
function resolveWorkerFilePath(branchPath, inputPath) {
|
|
679
|
+
if (typeof inputPath !== "string" || inputPath.length === 0 || inputPath.includes("\0")) {
|
|
680
|
+
throw new Error(`File path must be a non-empty string. Got: ${JSON.stringify(inputPath)}`);
|
|
673
681
|
}
|
|
674
|
-
const
|
|
675
|
-
if (
|
|
676
|
-
const
|
|
677
|
-
if (
|
|
678
|
-
return
|
|
682
|
+
const resolvedBranchPath = path.resolve(branchPath);
|
|
683
|
+
if (path.isAbsolute(inputPath)) {
|
|
684
|
+
const absolutePath2 = path.resolve(inputPath);
|
|
685
|
+
if (!isInsideBranchPath(resolvedBranchPath, absolutePath2)) {
|
|
686
|
+
return {
|
|
687
|
+
absolutePath: absolutePath2,
|
|
688
|
+
displayPath: absolutePath2,
|
|
689
|
+
repoRelativePath: null,
|
|
690
|
+
scope: "host"
|
|
691
|
+
};
|
|
679
692
|
}
|
|
693
|
+
const displayPath = toProjectDisplayPath(resolvedBranchPath, absolutePath2);
|
|
694
|
+
const repoRelativePath2 = displayPath === "." ? "" : displayPath;
|
|
695
|
+
assertAllowedProjectPath(repoRelativePath2, inputPath);
|
|
696
|
+
return {
|
|
697
|
+
absolutePath: absolutePath2,
|
|
698
|
+
displayPath,
|
|
699
|
+
repoRelativePath: repoRelativePath2,
|
|
700
|
+
scope: "project"
|
|
701
|
+
};
|
|
680
702
|
}
|
|
681
|
-
|
|
682
|
-
|
|
703
|
+
const normalizedInputPath = inputPath.replace(/\\/g, "/");
|
|
704
|
+
if (normalizedInputPath.split("/").includes("..")) {
|
|
705
|
+
throw new Error(`Invalid project file path: ${inputPath}`);
|
|
683
706
|
}
|
|
684
|
-
const normalized = path.posix.normalize(
|
|
685
|
-
|
|
686
|
-
|
|
707
|
+
const normalized = path.posix.normalize(normalizedInputPath);
|
|
708
|
+
const repoRelativePath = normalized === "." ? "" : normalized.replace(/^\.\/+/, "");
|
|
709
|
+
assertAllowedProjectPath(repoRelativePath, inputPath);
|
|
710
|
+
const absolutePath = repoRelativePath ? path.resolve(resolvedBranchPath, ...repoRelativePath.split("/")) : resolvedBranchPath;
|
|
711
|
+
if (!isInsideBranchPath(resolvedBranchPath, absolutePath)) {
|
|
712
|
+
throw new Error(`Invalid project file path: ${inputPath}`);
|
|
687
713
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
714
|
+
return {
|
|
715
|
+
absolutePath,
|
|
716
|
+
displayPath: repoRelativePath || ".",
|
|
717
|
+
repoRelativePath,
|
|
718
|
+
scope: "project"
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
function resolveProjectFilePath(branchPath, inputPath) {
|
|
722
|
+
const resolved = resolveWorkerFilePath(branchPath, inputPath);
|
|
723
|
+
if (resolved.scope === "host") {
|
|
724
|
+
throw new Error(
|
|
725
|
+
`Project file writes must use a checkout-relative path. Absolute host paths are read-only in file tools: ${inputPath}`
|
|
726
|
+
);
|
|
691
727
|
}
|
|
692
|
-
|
|
693
|
-
assertInsideBranch(branchPath, absolutePath);
|
|
694
|
-
return { absolutePath, virtualPath: repoRelativePath ? `/${repoRelativePath}` : "/", repoRelativePath };
|
|
728
|
+
return resolved;
|
|
695
729
|
}
|
|
696
730
|
function resolveRemoteUrl(baseUrl, remoteUrl) {
|
|
697
731
|
if (/^https?:\/\//i.test(remoteUrl)) {
|
|
@@ -1305,6 +1339,21 @@ async function withFileMutationQueue(key, operation) {
|
|
|
1305
1339
|
function mutationQueueKey(projectId, branchName, filePath) {
|
|
1306
1340
|
return `${projectId}:${branchName}:${path.posix.normalize(filePath)}`;
|
|
1307
1341
|
}
|
|
1342
|
+
function readWorkerTextFile(branchPath, filePath, offset, limit) {
|
|
1343
|
+
const resolved = resolveWorkerFilePath(branchPath, filePath);
|
|
1344
|
+
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
|
|
1345
|
+
throw new Error(`File not found: ${resolved.displayPath}`);
|
|
1346
|
+
}
|
|
1347
|
+
const buffer = fs.readFileSync(resolved.absolutePath);
|
|
1348
|
+
const formatted = formatLineNumberedContent(buffer.toString("utf8"), offset, limit);
|
|
1349
|
+
return {
|
|
1350
|
+
type: "read",
|
|
1351
|
+
kind: "text",
|
|
1352
|
+
file: resolved.displayPath,
|
|
1353
|
+
gitBlobHash: getGitBlobHashForContent(buffer),
|
|
1354
|
+
...formatted
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1308
1357
|
async function executeReadFileOperation(input) {
|
|
1309
1358
|
const artifact = await resolveArtifactEnvFilePath({
|
|
1310
1359
|
filePath: input.message.filePath,
|
|
@@ -1319,55 +1368,43 @@ async function executeReadFileOperation(input) {
|
|
|
1319
1368
|
if (!fs.existsSync(artifact.absolutePath) || !fs.statSync(artifact.absolutePath).isFile()) {
|
|
1320
1369
|
throw new Error(`File not found: ${artifact.virtualPath}`);
|
|
1321
1370
|
}
|
|
1322
|
-
const
|
|
1323
|
-
const
|
|
1371
|
+
const buffer = fs.readFileSync(artifact.absolutePath);
|
|
1372
|
+
const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
|
|
1324
1373
|
return {
|
|
1325
1374
|
type: "read",
|
|
1326
1375
|
kind: "text",
|
|
1327
1376
|
file: artifact.virtualPath,
|
|
1328
|
-
gitBlobHash: getGitBlobHashForContent(
|
|
1329
|
-
...
|
|
1377
|
+
gitBlobHash: getGitBlobHashForContent(buffer),
|
|
1378
|
+
...formatted
|
|
1330
1379
|
};
|
|
1331
1380
|
}
|
|
1332
1381
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1333
|
-
|
|
1334
|
-
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
|
|
1335
|
-
throw new Error(`File not found: ${resolved.virtualPath}`);
|
|
1336
|
-
}
|
|
1337
|
-
const buffer = fs.readFileSync(resolved.absolutePath);
|
|
1338
|
-
const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
|
|
1339
|
-
return {
|
|
1340
|
-
type: "read",
|
|
1341
|
-
kind: "text",
|
|
1342
|
-
file: resolved.virtualPath,
|
|
1343
|
-
gitBlobHash: getGitBlobHashForContent(buffer),
|
|
1344
|
-
...formatted
|
|
1345
|
-
};
|
|
1382
|
+
return readWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.offset, input.message.limit);
|
|
1346
1383
|
}
|
|
1347
1384
|
async function executeWriteFileOperation(input) {
|
|
1348
1385
|
rejectArtifactWritePath(input.message.filePath);
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1386
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1387
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
1388
|
+
return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved.repoRelativePath), async () => {
|
|
1352
1389
|
fs.mkdirSync(path.dirname(resolved.absolutePath), { recursive: true });
|
|
1353
1390
|
fs.writeFileSync(resolved.absolutePath, input.message.content, "utf8");
|
|
1354
1391
|
return {
|
|
1355
1392
|
type: "write",
|
|
1356
|
-
file: resolved.
|
|
1393
|
+
file: resolved.displayPath,
|
|
1357
1394
|
gitBlobHash: getGitBlobHashForContent(input.message.content)
|
|
1358
1395
|
};
|
|
1359
1396
|
});
|
|
1360
1397
|
}
|
|
1361
1398
|
async function executeEditFileOperation(input) {
|
|
1362
1399
|
rejectArtifactWritePath(input.message.filePath);
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1400
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1401
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
1402
|
+
return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved.repoRelativePath), async () => {
|
|
1366
1403
|
if (!Array.isArray(input.message.edits) || input.message.edits.length === 0) {
|
|
1367
1404
|
throw new Error("edit requires at least one replacement");
|
|
1368
1405
|
}
|
|
1369
1406
|
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
|
|
1370
|
-
throw new Error(`File not found: ${resolved.
|
|
1407
|
+
throw new Error(`File not found: ${resolved.displayPath}`);
|
|
1371
1408
|
}
|
|
1372
1409
|
const originalContent = fs.readFileSync(resolved.absolutePath, "utf8");
|
|
1373
1410
|
const ranges = input.message.edits.map((edit, index) => {
|
|
@@ -1402,7 +1439,7 @@ async function executeEditFileOperation(input) {
|
|
|
1402
1439
|
fs.writeFileSync(resolved.absolutePath, nextContent, "utf8");
|
|
1403
1440
|
return {
|
|
1404
1441
|
type: "edit",
|
|
1405
|
-
file: resolved.
|
|
1442
|
+
file: resolved.displayPath,
|
|
1406
1443
|
edits: input.message.edits.length
|
|
1407
1444
|
};
|
|
1408
1445
|
});
|
|
@@ -1422,47 +1459,70 @@ function wildcardToRegex(pattern) {
|
|
|
1422
1459
|
function normalizeFindPattern(pattern) {
|
|
1423
1460
|
return /[*?]/.test(pattern) ? pattern : `*${pattern}*`;
|
|
1424
1461
|
}
|
|
1425
|
-
function
|
|
1426
|
-
const relative = path.relative(branchPath, absolutePath).split(path.sep).join(path.posix.sep);
|
|
1427
|
-
return relative ? `/${relative}` : "/";
|
|
1428
|
-
}
|
|
1429
|
-
function walkProjectEntries(branchPath, startPath) {
|
|
1462
|
+
function walkWorkerEntries(branchPath, start) {
|
|
1430
1463
|
const entries = [];
|
|
1431
|
-
const
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1464
|
+
const visitedDirectories = /* @__PURE__ */ new Set();
|
|
1465
|
+
const visit = (absolutePath, isRoot) => {
|
|
1466
|
+
let stat;
|
|
1467
|
+
try {
|
|
1468
|
+
stat = fs.statSync(absolutePath);
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
if (isRoot) throw error;
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
const displayPath = start.scope === "project" ? toProjectDisplayPath(branchPath, absolutePath) : path.resolve(absolutePath);
|
|
1474
|
+
const hostRelativePath = path.relative(start.absolutePath, absolutePath).split(path.sep).join(path.posix.sep);
|
|
1475
|
+
const matchPath = start.scope === "project" ? displayPath : hostRelativePath || path.basename(start.absolutePath);
|
|
1476
|
+
entries.push({ absolutePath, displayPath, matchPath, stat });
|
|
1435
1477
|
if (!stat.isDirectory()) return;
|
|
1436
|
-
|
|
1478
|
+
let realDirectoryPath;
|
|
1479
|
+
try {
|
|
1480
|
+
realDirectoryPath = fs.realpathSync(absolutePath);
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
if (isRoot) throw error;
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
if (visitedDirectories.has(realDirectoryPath)) return;
|
|
1486
|
+
visitedDirectories.add(realDirectoryPath);
|
|
1487
|
+
let children;
|
|
1488
|
+
try {
|
|
1489
|
+
children = fs.readdirSync(absolutePath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
1490
|
+
} catch (error) {
|
|
1491
|
+
if (isRoot) throw error;
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1437
1494
|
for (const child of children) {
|
|
1438
1495
|
if (IGNORED_ENTRY_NAMES.has(child.name)) continue;
|
|
1439
|
-
visit(path.join(absolutePath, child.name));
|
|
1496
|
+
visit(path.join(absolutePath, child.name), false);
|
|
1440
1497
|
}
|
|
1441
1498
|
};
|
|
1442
|
-
visit(
|
|
1499
|
+
visit(start.absolutePath, true);
|
|
1443
1500
|
return entries;
|
|
1444
1501
|
}
|
|
1445
1502
|
function isProbablyText(bytes) {
|
|
1446
1503
|
return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
|
|
1447
1504
|
}
|
|
1448
|
-
|
|
1449
|
-
const
|
|
1450
|
-
const start = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
|
|
1505
|
+
function grepWorkerFiles(branchPath, input) {
|
|
1506
|
+
const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
|
|
1451
1507
|
if (!fs.existsSync(start.absolutePath)) {
|
|
1452
|
-
throw new Error(`Path not found: ${start.
|
|
1508
|
+
throw new Error(`Path not found: ${start.displayPath}`);
|
|
1453
1509
|
}
|
|
1454
|
-
const limit = normalizeToolLimit(input.
|
|
1455
|
-
const flags = input.
|
|
1456
|
-
const regex = new RegExp(input.
|
|
1457
|
-
const globRegex = input.
|
|
1510
|
+
const limit = normalizeToolLimit(input.limit, 100, 1e3);
|
|
1511
|
+
const flags = input.caseSensitive === false ? "i" : "";
|
|
1512
|
+
const regex = new RegExp(input.pattern, flags);
|
|
1513
|
+
const globRegex = input.glob ? wildcardToRegex(input.glob) : null;
|
|
1458
1514
|
const lines = [];
|
|
1459
1515
|
let matchCount = 0;
|
|
1460
1516
|
let truncated = false;
|
|
1461
|
-
for (const entry of
|
|
1517
|
+
for (const entry of walkWorkerEntries(branchPath, start)) {
|
|
1462
1518
|
if (!entry.stat.isFile()) continue;
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1519
|
+
if (globRegex && !globRegex.test(entry.matchPath)) continue;
|
|
1520
|
+
let bytes;
|
|
1521
|
+
try {
|
|
1522
|
+
bytes = fs.readFileSync(entry.absolutePath);
|
|
1523
|
+
} catch {
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1466
1526
|
if (!isProbablyText(bytes)) continue;
|
|
1467
1527
|
const fileLines = bytes.toString("utf8").split(/\r?\n/);
|
|
1468
1528
|
for (let index = 0; index < fileLines.length; index += 1) {
|
|
@@ -1473,7 +1533,7 @@ async function executeGrepOperation(input) {
|
|
|
1473
1533
|
matchCount += 1;
|
|
1474
1534
|
if (lines.length < limit) {
|
|
1475
1535
|
const column = Math.max(1, (match.index ?? 0) + 1);
|
|
1476
|
-
lines.push(`${entry.
|
|
1536
|
+
lines.push(`${entry.displayPath}:${index + 1}:${column}: ${line.slice(0, MAX_LINE_LENGTH)}`);
|
|
1477
1537
|
} else {
|
|
1478
1538
|
truncated = true;
|
|
1479
1539
|
}
|
|
@@ -1488,26 +1548,35 @@ async function executeGrepOperation(input) {
|
|
|
1488
1548
|
truncated
|
|
1489
1549
|
};
|
|
1490
1550
|
}
|
|
1491
|
-
async function
|
|
1551
|
+
async function executeGrepOperation(input) {
|
|
1492
1552
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1493
|
-
|
|
1553
|
+
return grepWorkerFiles(workspace.branchPath, {
|
|
1554
|
+
pattern: input.message.pattern,
|
|
1555
|
+
path: input.message.path,
|
|
1556
|
+
glob: input.message.glob,
|
|
1557
|
+
caseSensitive: input.message.caseSensitive,
|
|
1558
|
+
limit: input.message.limit
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
function findWorkerFiles(branchPath, input) {
|
|
1562
|
+
const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
|
|
1494
1563
|
if (!fs.existsSync(start.absolutePath) || !fs.statSync(start.absolutePath).isDirectory()) {
|
|
1495
|
-
throw new Error(`Directory not found: ${start.
|
|
1564
|
+
throw new Error(`Directory not found: ${start.displayPath}`);
|
|
1496
1565
|
}
|
|
1497
|
-
const pattern = normalizeFindPattern(input.
|
|
1566
|
+
const pattern = normalizeFindPattern(input.pattern ?? "*");
|
|
1498
1567
|
const matcher = wildcardToRegex(pattern);
|
|
1499
|
-
const entryType = input.
|
|
1500
|
-
const limit = normalizeToolLimit(input.
|
|
1568
|
+
const entryType = input.entryType ?? "file";
|
|
1569
|
+
const limit = normalizeToolLimit(input.limit, 200, 1e3);
|
|
1501
1570
|
const matches = [];
|
|
1502
1571
|
let count = 0;
|
|
1503
1572
|
let truncated = false;
|
|
1504
|
-
for (const entry of
|
|
1505
|
-
if (entry.
|
|
1573
|
+
for (const entry of walkWorkerEntries(branchPath, start)) {
|
|
1574
|
+
if (entry.absolutePath === start.absolutePath) continue;
|
|
1506
1575
|
const isDirectory = entry.stat.isDirectory();
|
|
1507
1576
|
if (entryType === "file" && !entry.stat.isFile()) continue;
|
|
1508
1577
|
if (entryType === "directory" && !isDirectory) continue;
|
|
1509
|
-
const label = isDirectory ? `${entry.
|
|
1510
|
-
if (!matcher.test(path.
|
|
1578
|
+
const label = isDirectory ? `${entry.displayPath}${start.scope === "host" ? path.sep : "/"}` : entry.displayPath;
|
|
1579
|
+
if (!matcher.test(path.basename(entry.absolutePath)) && !matcher.test(entry.matchPath)) continue;
|
|
1511
1580
|
count += 1;
|
|
1512
1581
|
if (matches.length < limit) {
|
|
1513
1582
|
matches.push(label);
|
|
@@ -1523,25 +1592,59 @@ async function executeFindOperation(input) {
|
|
|
1523
1592
|
truncated
|
|
1524
1593
|
};
|
|
1525
1594
|
}
|
|
1526
|
-
async function
|
|
1595
|
+
async function executeFindOperation(input) {
|
|
1527
1596
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1528
|
-
|
|
1597
|
+
return findWorkerFiles(workspace.branchPath, {
|
|
1598
|
+
pattern: input.message.pattern,
|
|
1599
|
+
path: input.message.path,
|
|
1600
|
+
entryType: input.message.entryType,
|
|
1601
|
+
limit: input.message.limit
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
function listWorkerDirectory(branchPath, inputPath = ".", inputLimit) {
|
|
1605
|
+
const resolved = resolveWorkerFilePath(branchPath, inputPath);
|
|
1529
1606
|
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isDirectory()) {
|
|
1530
|
-
throw new Error(`Directory not found: ${resolved.
|
|
1607
|
+
throw new Error(`Directory not found: ${resolved.displayPath}`);
|
|
1531
1608
|
}
|
|
1532
|
-
const limit = normalizeToolLimit(
|
|
1609
|
+
const limit = normalizeToolLimit(inputLimit, 200, 1e3);
|
|
1533
1610
|
const entries = fs.readdirSync(resolved.absolutePath, { withFileTypes: true }).filter((entry) => !IGNORED_ENTRY_NAMES.has(entry.name)).sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name));
|
|
1534
1611
|
const displayed = entries.slice(0, limit).map((entry) => `${entry.name}${entry.isDirectory() ? "/" : ""}`);
|
|
1535
1612
|
const truncated = entries.length > displayed.length;
|
|
1536
1613
|
const hint = truncated ? "\n... truncated. Increase limit to continue." : "";
|
|
1537
1614
|
return {
|
|
1538
1615
|
type: "ls",
|
|
1539
|
-
path: resolved.
|
|
1616
|
+
path: resolved.displayPath,
|
|
1540
1617
|
content: displayed.length > 0 ? `${displayed.join("\n")}${hint}` : "(empty directory)",
|
|
1541
1618
|
count: entries.length,
|
|
1542
1619
|
truncated
|
|
1543
1620
|
};
|
|
1544
1621
|
}
|
|
1622
|
+
async function executeLsOperation(input) {
|
|
1623
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1624
|
+
return listWorkerDirectory(workspace.branchPath, input.message.path, input.message.limit);
|
|
1625
|
+
}
|
|
1626
|
+
function readWorkerImageFile(branchPath, filePath) {
|
|
1627
|
+
const resolved = resolveWorkerFilePath(branchPath, filePath);
|
|
1628
|
+
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
|
|
1629
|
+
throw new Error(`File not found: ${resolved.displayPath}`);
|
|
1630
|
+
}
|
|
1631
|
+
const extension = path.extname(resolved.absolutePath).toLowerCase();
|
|
1632
|
+
const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
|
|
1633
|
+
if (!mediaType) {
|
|
1634
|
+
throw new Error("read supports PNG and JPEG image inputs only");
|
|
1635
|
+
}
|
|
1636
|
+
const bytes = fs.readFileSync(resolved.absolutePath);
|
|
1637
|
+
const dimensions = readImageDimensions(bytes, mediaType);
|
|
1638
|
+
return {
|
|
1639
|
+
type: "view_file_bytes",
|
|
1640
|
+
filePath: resolved.displayPath,
|
|
1641
|
+
mediaType,
|
|
1642
|
+
base64: bytes.toString("base64"),
|
|
1643
|
+
fileSizeBytes: bytes.length,
|
|
1644
|
+
width: dimensions.width,
|
|
1645
|
+
height: dimensions.height
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1545
1648
|
async function executeViewFileBytesOperation(input) {
|
|
1546
1649
|
const artifact = await resolveArtifactEnvFilePath({
|
|
1547
1650
|
filePath: input.message.filePath,
|
|
@@ -1556,44 +1659,25 @@ async function executeViewFileBytesOperation(input) {
|
|
|
1556
1659
|
if (!fs.existsSync(artifact.absolutePath) || !fs.statSync(artifact.absolutePath).isFile()) {
|
|
1557
1660
|
throw new Error(`File not found: ${artifact.virtualPath}`);
|
|
1558
1661
|
}
|
|
1559
|
-
const
|
|
1560
|
-
const
|
|
1561
|
-
if (!
|
|
1662
|
+
const extension = path.extname(artifact.absolutePath).toLowerCase();
|
|
1663
|
+
const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
|
|
1664
|
+
if (!mediaType) {
|
|
1562
1665
|
throw new Error("read supports PNG and JPEG image inputs only");
|
|
1563
1666
|
}
|
|
1564
|
-
const
|
|
1565
|
-
const
|
|
1667
|
+
const bytes = fs.readFileSync(artifact.absolutePath);
|
|
1668
|
+
const dimensions = readImageDimensions(bytes, mediaType);
|
|
1566
1669
|
return {
|
|
1567
1670
|
type: "view_file_bytes",
|
|
1568
1671
|
filePath: artifact.virtualPath,
|
|
1569
|
-
mediaType
|
|
1570
|
-
base64:
|
|
1571
|
-
fileSizeBytes:
|
|
1572
|
-
width:
|
|
1573
|
-
height:
|
|
1672
|
+
mediaType,
|
|
1673
|
+
base64: bytes.toString("base64"),
|
|
1674
|
+
fileSizeBytes: bytes.length,
|
|
1675
|
+
width: dimensions.width,
|
|
1676
|
+
height: dimensions.height
|
|
1574
1677
|
};
|
|
1575
1678
|
}
|
|
1576
1679
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1577
|
-
|
|
1578
|
-
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
|
|
1579
|
-
throw new Error(`File not found: ${resolved.virtualPath}`);
|
|
1580
|
-
}
|
|
1581
|
-
const extension = path.extname(resolved.absolutePath).toLowerCase();
|
|
1582
|
-
const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
|
|
1583
|
-
if (!mediaType) {
|
|
1584
|
-
throw new Error("read supports PNG and JPEG image inputs only");
|
|
1585
|
-
}
|
|
1586
|
-
const bytes = fs.readFileSync(resolved.absolutePath);
|
|
1587
|
-
const dimensions = readImageDimensions(bytes, mediaType);
|
|
1588
|
-
return {
|
|
1589
|
-
type: "view_file_bytes",
|
|
1590
|
-
filePath: resolved.virtualPath,
|
|
1591
|
-
mediaType,
|
|
1592
|
-
base64: bytes.toString("base64"),
|
|
1593
|
-
fileSizeBytes: bytes.length,
|
|
1594
|
-
width: dimensions.width,
|
|
1595
|
-
height: dimensions.height
|
|
1596
|
-
};
|
|
1680
|
+
return readWorkerImageFile(workspace.branchPath, input.message.filePath);
|
|
1597
1681
|
}
|
|
1598
1682
|
function stagedBlobSizeBytes(context) {
|
|
1599
1683
|
const names = runInternalGit(context, ["diff", "--cached", "--name-only", "-z", "--", ".", ":(exclude).git"]).split("\0").filter(Boolean);
|
|
@@ -1774,6 +1858,7 @@ async function executeCommand(input) {
|
|
|
1774
1858
|
cwd,
|
|
1775
1859
|
stdout: "pipe",
|
|
1776
1860
|
stderr: "pipe",
|
|
1861
|
+
detached: true,
|
|
1777
1862
|
env: {
|
|
1778
1863
|
...process.env,
|
|
1779
1864
|
...containerRegistryEnv(),
|
|
@@ -1795,7 +1880,12 @@ async function executeCommand(input) {
|
|
|
1795
1880
|
});
|
|
1796
1881
|
if (input.message.timeoutMs) {
|
|
1797
1882
|
timeout = setTimeout(() => {
|
|
1798
|
-
subprocess.
|
|
1883
|
+
void terminateProcessTree(subprocess).catch((error) => {
|
|
1884
|
+
process.stderr.write(
|
|
1885
|
+
`[r5d-worker] failed to terminate timed-out command ${input.message.runId}: ${error instanceof Error ? error.message : String(error)}
|
|
1886
|
+
`
|
|
1887
|
+
);
|
|
1888
|
+
});
|
|
1799
1889
|
}, input.message.timeoutMs);
|
|
1800
1890
|
}
|
|
1801
1891
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
@@ -1831,6 +1921,8 @@ async function executeCommand(input) {
|
|
|
1831
1921
|
async function executeStreamingCommand(input) {
|
|
1832
1922
|
const startedAt = Date.now();
|
|
1833
1923
|
let started = false;
|
|
1924
|
+
let timedOut = false;
|
|
1925
|
+
let timeout;
|
|
1834
1926
|
try {
|
|
1835
1927
|
if (cancelledProcessRuns.delete(input.message.runId)) {
|
|
1836
1928
|
sendWorkerMessage(input.ws, {
|
|
@@ -1871,6 +1963,7 @@ async function executeStreamingCommand(input) {
|
|
|
1871
1963
|
cwd,
|
|
1872
1964
|
stdout: "pipe",
|
|
1873
1965
|
stderr: "pipe",
|
|
1966
|
+
detached: true,
|
|
1874
1967
|
env: {
|
|
1875
1968
|
...process.env,
|
|
1876
1969
|
...containerRegistryEnv(),
|
|
@@ -1897,6 +1990,17 @@ async function executeStreamingCommand(input) {
|
|
|
1897
1990
|
requestId: input.message.requestId,
|
|
1898
1991
|
runId: input.message.runId
|
|
1899
1992
|
});
|
|
1993
|
+
if (input.message.timeoutMs) {
|
|
1994
|
+
timeout = setTimeout(() => {
|
|
1995
|
+
timedOut = true;
|
|
1996
|
+
void terminateProcessTree(subprocess).catch((error) => {
|
|
1997
|
+
process.stderr.write(
|
|
1998
|
+
`[r5d-worker] failed to terminate timed-out process ${input.message.runId}: ${error instanceof Error ? error.message : String(error)}
|
|
1999
|
+
`
|
|
2000
|
+
);
|
|
2001
|
+
});
|
|
2002
|
+
}, input.message.timeoutMs);
|
|
2003
|
+
}
|
|
1900
2004
|
const [exitCode] = await Promise.all([
|
|
1901
2005
|
subprocess.exited,
|
|
1902
2006
|
streamCommandOutput(subprocess.stdout, (data) => {
|
|
@@ -1920,7 +2024,8 @@ async function executeStreamingCommand(input) {
|
|
|
1920
2024
|
type: "exec_exit",
|
|
1921
2025
|
runId: input.message.runId,
|
|
1922
2026
|
exitCode,
|
|
1923
|
-
durationMs: Date.now() - startedAt
|
|
2027
|
+
durationMs: Date.now() - startedAt,
|
|
2028
|
+
...timedOut ? { timedOut: true } : {}
|
|
1924
2029
|
});
|
|
1925
2030
|
} catch (error) {
|
|
1926
2031
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1940,6 +2045,9 @@ async function executeStreamingCommand(input) {
|
|
|
1940
2045
|
});
|
|
1941
2046
|
}
|
|
1942
2047
|
} finally {
|
|
2048
|
+
if (timeout) {
|
|
2049
|
+
clearTimeout(timeout);
|
|
2050
|
+
}
|
|
1943
2051
|
activeProcesses.delete(input.message.runId);
|
|
1944
2052
|
cancelledProcessRuns.delete(input.message.runId);
|
|
1945
2053
|
}
|
|
@@ -2502,18 +2610,27 @@ async function startWorker(options) {
|
|
|
2502
2610
|
}
|
|
2503
2611
|
if (message.type === "cancel") {
|
|
2504
2612
|
const active = activeProcesses.get(message.runId);
|
|
2613
|
+
let cancelled = true;
|
|
2614
|
+
let cancelMessage;
|
|
2505
2615
|
if (active) {
|
|
2506
|
-
|
|
2616
|
+
try {
|
|
2617
|
+
await terminateProcessTree(active.process);
|
|
2618
|
+
cancelMessage = `Stopped ${active.command}`;
|
|
2619
|
+
} catch (error) {
|
|
2620
|
+
cancelled = false;
|
|
2621
|
+
cancelMessage = `Failed to stop ${active.command}: ${error instanceof Error ? error.message : String(error)}`;
|
|
2622
|
+
}
|
|
2507
2623
|
} else {
|
|
2508
2624
|
cancelledProcessRuns.add(message.runId);
|
|
2625
|
+
cancelMessage = "Cancellation queued before command start";
|
|
2509
2626
|
}
|
|
2510
2627
|
ws.send(
|
|
2511
2628
|
JSON.stringify({
|
|
2512
2629
|
type: "cancel_result",
|
|
2513
2630
|
requestId: message.requestId,
|
|
2514
2631
|
runId: message.runId,
|
|
2515
|
-
cancelled
|
|
2516
|
-
message:
|
|
2632
|
+
cancelled,
|
|
2633
|
+
message: cancelMessage
|
|
2517
2634
|
})
|
|
2518
2635
|
);
|
|
2519
2636
|
return;
|
|
@@ -2752,11 +2869,17 @@ if (isCliEntrypoint()) {
|
|
|
2752
2869
|
export {
|
|
2753
2870
|
ensureVisibleGitCheckout,
|
|
2754
2871
|
findRepositorySyncBlockers,
|
|
2872
|
+
findWorkerFiles,
|
|
2873
|
+
grepWorkerFiles,
|
|
2755
2874
|
isArtifactEnvPath,
|
|
2875
|
+
listWorkerDirectory,
|
|
2756
2876
|
prepareArtifactEnvForShell,
|
|
2757
2877
|
preparePlanEnvForShell,
|
|
2878
|
+
readWorkerImageFile,
|
|
2879
|
+
readWorkerTextFile,
|
|
2758
2880
|
resolveHostShell,
|
|
2759
2881
|
resolveProjectFilePath,
|
|
2882
|
+
resolveWorkerFilePath,
|
|
2760
2883
|
syncManifestProjectsFromInternal,
|
|
2761
2884
|
syncProjectPlans,
|
|
2762
2885
|
syncSessionArtifacts
|
package/dist/mjs/package.json
CHANGED