@locusai/cli 0.22.6 → 0.22.8
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/bin/locus.js +143 -23
- package/package.json +2 -2
package/bin/locus.js
CHANGED
|
@@ -892,6 +892,7 @@ var init_progress = __esm(() => {
|
|
|
892
892
|
var exports_sandbox = {};
|
|
893
893
|
__export(exports_sandbox, {
|
|
894
894
|
resolveSandboxMode: () => resolveSandboxMode,
|
|
895
|
+
probeSymlinkSupport: () => probeSymlinkSupport,
|
|
895
896
|
getProviderSandboxName: () => getProviderSandboxName,
|
|
896
897
|
getModelSandboxName: () => getModelSandboxName,
|
|
897
898
|
displaySandboxWarning: () => displaySandboxWarning,
|
|
@@ -1054,7 +1055,7 @@ function detectContainerWorkdir(sandboxName, hostProjectRoot) {
|
|
|
1054
1055
|
}).filter((mp) => !!mp && mp !== "/" && !systemPrefixes.some((p) => mp === p || mp.startsWith(`${p}/`)));
|
|
1055
1056
|
for (const candidate of candidates) {
|
|
1056
1057
|
try {
|
|
1057
|
-
execSync2(`docker sandbox exec ${sandboxName} sh -c "test -d ${JSON.stringify(candidate
|
|
1058
|
+
execSync2(`docker sandbox exec ${sandboxName} sh -c "test -d ${JSON.stringify(`${candidate}/`)} || test -f ${JSON.stringify(`${candidate}/package.json`)}"`, { stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
|
|
1058
1059
|
log.debug("Detected container workdir from mount table", {
|
|
1059
1060
|
hostProjectRoot,
|
|
1060
1061
|
containerWorkdir: candidate
|
|
@@ -1086,6 +1087,21 @@ function detectContainerWorkdir(sandboxName, hostProjectRoot) {
|
|
|
1086
1087
|
log.debug("Could not detect container workdir, falling back to host path");
|
|
1087
1088
|
return null;
|
|
1088
1089
|
}
|
|
1090
|
+
function probeSymlinkSupport(sandboxName, workdir) {
|
|
1091
|
+
const log = getLogger();
|
|
1092
|
+
const probe = ".locus-symlink-probe";
|
|
1093
|
+
try {
|
|
1094
|
+
execSync2(`docker sandbox exec --privileged ${sandboxName} sh -c ${JSON.stringify(`cd ${JSON.stringify(workdir)} && ln -s /tmp ${probe} && rm -f ${probe}`)}`, { stdio: ["pipe", "pipe", "pipe"], timeout: 5000 });
|
|
1095
|
+
log.debug("Symlink probe succeeded — bind mount supports symlinks");
|
|
1096
|
+
return true;
|
|
1097
|
+
} catch {
|
|
1098
|
+
try {
|
|
1099
|
+
execSync2(`docker sandbox exec ${sandboxName} rm -f ${JSON.stringify(`${workdir}/${probe}`)}`, { stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
|
|
1100
|
+
} catch {}
|
|
1101
|
+
log.debug("Symlink probe failed — bind mount does not support symlinks (ENOSYS)");
|
|
1102
|
+
return false;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1089
1105
|
function waitForEnter() {
|
|
1090
1106
|
return new Promise((resolve) => {
|
|
1091
1107
|
const rl = createInterface({
|
|
@@ -5208,7 +5224,7 @@ async function enforceSandboxIgnore(sandboxName, projectRoot, containerWorkdir)
|
|
|
5208
5224
|
ruleCount: rules.length
|
|
5209
5225
|
});
|
|
5210
5226
|
try {
|
|
5211
|
-
await execAsync(`docker sandbox exec ${sandboxName} sh -c ${JSON.stringify(script)}`, { timeout: 15000 });
|
|
5227
|
+
await execAsync(`docker sandbox exec --privileged ${sandboxName} sh -c ${JSON.stringify(script)}`, { timeout: 15000 });
|
|
5212
5228
|
log.debug("sandbox-ignore enforcement complete", { sandboxName });
|
|
5213
5229
|
} catch (err) {
|
|
5214
5230
|
log.debug("sandbox-ignore enforcement failed (non-fatal)", {
|
|
@@ -5278,6 +5294,7 @@ class SandboxedClaudeRunner {
|
|
|
5278
5294
|
const dockerArgs = [
|
|
5279
5295
|
"sandbox",
|
|
5280
5296
|
"exec",
|
|
5297
|
+
"--privileged",
|
|
5281
5298
|
"-w",
|
|
5282
5299
|
workdir,
|
|
5283
5300
|
this.sandboxName,
|
|
@@ -5665,6 +5682,7 @@ class SandboxedCodexRunner {
|
|
|
5665
5682
|
const dockerArgs = [
|
|
5666
5683
|
"sandbox",
|
|
5667
5684
|
"exec",
|
|
5685
|
+
"--privileged",
|
|
5668
5686
|
"-i",
|
|
5669
5687
|
"-w",
|
|
5670
5688
|
workdir,
|
|
@@ -5818,11 +5836,11 @@ class SandboxedCodexRunner {
|
|
|
5818
5836
|
const { exec: exec2 } = await import("node:child_process");
|
|
5819
5837
|
const execAsync2 = promisify2(exec2);
|
|
5820
5838
|
try {
|
|
5821
|
-
await execAsync2(`docker sandbox exec ${name} which codex`, {
|
|
5839
|
+
await execAsync2(`docker sandbox exec --privileged ${name} which codex`, {
|
|
5822
5840
|
timeout: 5000
|
|
5823
5841
|
});
|
|
5824
5842
|
} catch {
|
|
5825
|
-
await execAsync2(`docker sandbox exec ${name} npm install -g @openai/codex`, {
|
|
5843
|
+
await execAsync2(`docker sandbox exec --privileged ${name} npm install -g @openai/codex`, {
|
|
5826
5844
|
timeout: 120000
|
|
5827
5845
|
});
|
|
5828
5846
|
}
|
|
@@ -13150,6 +13168,14 @@ async function handleCreate(projectRoot) {
|
|
|
13150
13168
|
}
|
|
13151
13169
|
}
|
|
13152
13170
|
const workdir = config.sandbox.containerWorkdir ?? projectRoot;
|
|
13171
|
+
if (config.sandbox.noSymlinks === undefined) {
|
|
13172
|
+
const symlinkOk = probeSymlinkSupport(name, workdir);
|
|
13173
|
+
config.sandbox.noSymlinks = !symlinkOk;
|
|
13174
|
+
if (!symlinkOk) {
|
|
13175
|
+
process.stderr.write(` ${yellow2("⚠")} Bind mount does not support symlinks — using copy-based install.
|
|
13176
|
+
`);
|
|
13177
|
+
}
|
|
13178
|
+
}
|
|
13153
13179
|
const backup = backupIgnoredFiles(projectRoot);
|
|
13154
13180
|
try {
|
|
13155
13181
|
await enforceSandboxIgnore(name, projectRoot, config.sandbox.containerWorkdir);
|
|
@@ -13160,7 +13186,7 @@ async function handleCreate(projectRoot) {
|
|
|
13160
13186
|
config.sandbox.enabled = true;
|
|
13161
13187
|
config.sandbox.providers = readySandboxes;
|
|
13162
13188
|
saveConfig(projectRoot, config);
|
|
13163
|
-
await runSandboxSetup(name, projectRoot,
|
|
13189
|
+
await runSandboxSetup(name, projectRoot, config.sandbox.containerWorkdir, config.sandbox.noSymlinks);
|
|
13164
13190
|
process.stderr.write(`
|
|
13165
13191
|
${green("✓")} Sandbox mode enabled for ${bold2(provider)}.
|
|
13166
13192
|
`);
|
|
@@ -13369,6 +13395,7 @@ async function handleInstall(projectRoot, args) {
|
|
|
13369
13395
|
const ok = await runInteractiveCommand("docker", [
|
|
13370
13396
|
"sandbox",
|
|
13371
13397
|
"exec",
|
|
13398
|
+
"--privileged",
|
|
13372
13399
|
sandboxName,
|
|
13373
13400
|
"npm",
|
|
13374
13401
|
"install",
|
|
@@ -13511,10 +13538,10 @@ function detectPackageManager2(projectRoot) {
|
|
|
13511
13538
|
}
|
|
13512
13539
|
return "npm";
|
|
13513
13540
|
}
|
|
13514
|
-
function getInstallCommand(pm
|
|
13541
|
+
function getInstallCommand(pm) {
|
|
13515
13542
|
switch (pm) {
|
|
13516
13543
|
case "bun":
|
|
13517
|
-
return
|
|
13544
|
+
return ["bun", "install"];
|
|
13518
13545
|
case "yarn":
|
|
13519
13546
|
return ["yarn", "install"];
|
|
13520
13547
|
case "pnpm":
|
|
@@ -13523,7 +13550,79 @@ function getInstallCommand(pm, noSymlinks) {
|
|
|
13523
13550
|
return ["npm", "install"];
|
|
13524
13551
|
}
|
|
13525
13552
|
}
|
|
13526
|
-
|
|
13553
|
+
function verifyBinEntries(sandboxName, workdir) {
|
|
13554
|
+
try {
|
|
13555
|
+
const binDir = `${workdir}/node_modules/.bin/`;
|
|
13556
|
+
const result = execSync20(`docker sandbox exec --privileged ${sandboxName} sh -c ${JSON.stringify(`ls ${JSON.stringify(binDir)} 2>/dev/null | head -20`)}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
|
|
13557
|
+
if (!result) {
|
|
13558
|
+
process.stderr.write(`${yellow2("⚠")} node_modules/.bin is empty — binaries like biome may not be available.
|
|
13559
|
+
`);
|
|
13560
|
+
process.stderr.write(` ${dim2("This can happen if symlink dereferencing failed during install.")}
|
|
13561
|
+
`);
|
|
13562
|
+
return;
|
|
13563
|
+
}
|
|
13564
|
+
const bins = result.split(`
|
|
13565
|
+
`).map((b) => b.trim());
|
|
13566
|
+
const knownBins = ["biome", "tsc", "jest"];
|
|
13567
|
+
const missing = knownBins.filter((b) => !bins.some((entry) => entry === b));
|
|
13568
|
+
if (missing.length > 0) {
|
|
13569
|
+
process.stderr.write(` ${dim2(`Note: ${missing.join(", ")} not found in .bin (may not be a project dependency).`)}
|
|
13570
|
+
`);
|
|
13571
|
+
}
|
|
13572
|
+
} catch {}
|
|
13573
|
+
}
|
|
13574
|
+
function buildNoSymlinksInstallScript(workdir, pm) {
|
|
13575
|
+
const installCmd = getInstallCommand(pm).join(" ");
|
|
13576
|
+
return [
|
|
13577
|
+
"set -e",
|
|
13578
|
+
'TMPDIR="/tmp/locus-deps-install"',
|
|
13579
|
+
`WORKDIR="${workdir}"`,
|
|
13580
|
+
"",
|
|
13581
|
+
"# Clean previous attempt",
|
|
13582
|
+
'rm -rf "$TMPDIR"',
|
|
13583
|
+
'mkdir -p "$TMPDIR"',
|
|
13584
|
+
'cd "$WORKDIR"',
|
|
13585
|
+
"",
|
|
13586
|
+
"# Copy all package.json files (preserving workspace directory structure)",
|
|
13587
|
+
'find . -name package.json -not -path "*/node_modules/*" -not -path "./.git/*" | while read f; do',
|
|
13588
|
+
' mkdir -p "$TMPDIR/$(dirname "$f")"',
|
|
13589
|
+
' cp "$f" "$TMPDIR/$f"',
|
|
13590
|
+
"done",
|
|
13591
|
+
"",
|
|
13592
|
+
"# Copy lockfiles and config files that affect installation",
|
|
13593
|
+
"for lf in bun.lock bun.lockb yarn.lock pnpm-lock.yaml package-lock.json; do",
|
|
13594
|
+
' [ -f "$WORKDIR/$lf" ] && cp "$WORKDIR/$lf" "$TMPDIR/$lf" || true',
|
|
13595
|
+
"done",
|
|
13596
|
+
"for cf in bunfig.toml .npmrc .yarnrc .yarnrc.yml .pnpmfile.cjs pnpm-workspace.yaml; do",
|
|
13597
|
+
' [ -f "$WORKDIR/$cf" ] && cp "$WORKDIR/$cf" "$TMPDIR/$cf" || true',
|
|
13598
|
+
"done",
|
|
13599
|
+
"",
|
|
13600
|
+
"# Install on native filesystem (symlinks work here)",
|
|
13601
|
+
'cd "$TMPDIR"',
|
|
13602
|
+
`${installCmd}`,
|
|
13603
|
+
"",
|
|
13604
|
+
"# Copy node_modules back to bind mount, dereferencing all symlinks.",
|
|
13605
|
+
"# -L follows symlinks so they become regular files/dirs on the bind mount.",
|
|
13606
|
+
"# maxdepth 5 covers root + nested workspace packages",
|
|
13607
|
+
'find "$TMPDIR" -maxdepth 5 -name node_modules -type d | while read nm; do',
|
|
13608
|
+
' rel="${nm#$TMPDIR/}"',
|
|
13609
|
+
' dest="$WORKDIR/$rel"',
|
|
13610
|
+
' rm -rf "$dest"',
|
|
13611
|
+
" # cp -rL dereferences symlinks; fall back to cp -rH if -L is unsupported",
|
|
13612
|
+
' cp -rL "$nm" "$dest" 2>/dev/null || cp -rH "$nm" "$dest" 2>/dev/null || cp -r "$nm" "$dest"',
|
|
13613
|
+
"done",
|
|
13614
|
+
"",
|
|
13615
|
+
"# Ensure .bin entries are executable (some cp implementations drop +x)",
|
|
13616
|
+
'if [ -d "$WORKDIR/node_modules/.bin" ]; then',
|
|
13617
|
+
' chmod +x "$WORKDIR/node_modules/.bin/"* 2>/dev/null || true',
|
|
13618
|
+
"fi",
|
|
13619
|
+
"",
|
|
13620
|
+
"# Cleanup",
|
|
13621
|
+
'rm -rf "$TMPDIR"'
|
|
13622
|
+
].join(`
|
|
13623
|
+
`);
|
|
13624
|
+
}
|
|
13625
|
+
async function runSandboxSetup(sandboxName, projectRoot, containerWorkdir, noSymlinks) {
|
|
13527
13626
|
const workdir = containerWorkdir ?? projectRoot;
|
|
13528
13627
|
const ecosystem = detectProjectEcosystem(projectRoot);
|
|
13529
13628
|
const isJS = isJavaScriptEcosystem(ecosystem);
|
|
@@ -13532,18 +13631,37 @@ async function runSandboxSetup(sandboxName, projectRoot, containerWorkdir) {
|
|
|
13532
13631
|
if (pm !== "npm") {
|
|
13533
13632
|
await ensurePackageManagerInSandbox(sandboxName, pm);
|
|
13534
13633
|
}
|
|
13535
|
-
|
|
13536
|
-
|
|
13634
|
+
let installOk;
|
|
13635
|
+
if (noSymlinks) {
|
|
13636
|
+
const installCmd = getInstallCommand(pm);
|
|
13637
|
+
process.stderr.write(`
|
|
13638
|
+
Installing dependencies (${bold2(installCmd.join(" "))}, symlink-free) in sandbox ${dim2(sandboxName)}...
|
|
13639
|
+
`);
|
|
13640
|
+
const script = buildNoSymlinksInstallScript(workdir, pm);
|
|
13641
|
+
installOk = await runInteractiveCommand("docker", [
|
|
13642
|
+
"sandbox",
|
|
13643
|
+
"exec",
|
|
13644
|
+
"--privileged",
|
|
13645
|
+
sandboxName,
|
|
13646
|
+
"sh",
|
|
13647
|
+
"-c",
|
|
13648
|
+
script
|
|
13649
|
+
]);
|
|
13650
|
+
} else {
|
|
13651
|
+
const installCmd = getInstallCommand(pm);
|
|
13652
|
+
process.stderr.write(`
|
|
13537
13653
|
Installing dependencies (${bold2(installCmd.join(" "))}) in sandbox ${dim2(sandboxName)}...
|
|
13538
13654
|
`);
|
|
13539
|
-
|
|
13540
|
-
|
|
13541
|
-
|
|
13542
|
-
|
|
13543
|
-
|
|
13544
|
-
|
|
13545
|
-
|
|
13546
|
-
|
|
13655
|
+
installOk = await runInteractiveCommand("docker", [
|
|
13656
|
+
"sandbox",
|
|
13657
|
+
"exec",
|
|
13658
|
+
"--privileged",
|
|
13659
|
+
"-w",
|
|
13660
|
+
workdir,
|
|
13661
|
+
sandboxName,
|
|
13662
|
+
...installCmd
|
|
13663
|
+
]);
|
|
13664
|
+
}
|
|
13547
13665
|
if (!installOk) {
|
|
13548
13666
|
process.stderr.write(`${red2("✗")} Dependency install failed in sandbox ${dim2(sandboxName)}.
|
|
13549
13667
|
`);
|
|
@@ -13551,6 +13669,7 @@ Installing dependencies (${bold2(installCmd.join(" "))}) in sandbox ${dim2(sandb
|
|
|
13551
13669
|
}
|
|
13552
13670
|
process.stderr.write(`${green("✓")} Dependencies installed in sandbox ${dim2(sandboxName)}.
|
|
13553
13671
|
`);
|
|
13672
|
+
verifyBinEntries(sandboxName, workdir);
|
|
13554
13673
|
} else {
|
|
13555
13674
|
process.stderr.write(`
|
|
13556
13675
|
${dim2(`Detected ${ecosystem} project — skipping JS package install.`)}
|
|
@@ -13564,6 +13683,7 @@ ${dim2(`Detected ${ecosystem} project — skipping JS package install.`)}
|
|
|
13564
13683
|
const hookOk = await runInteractiveCommand("docker", [
|
|
13565
13684
|
"sandbox",
|
|
13566
13685
|
"exec",
|
|
13686
|
+
"--privileged",
|
|
13567
13687
|
"-w",
|
|
13568
13688
|
workdir,
|
|
13569
13689
|
sandboxName,
|
|
@@ -13599,7 +13719,7 @@ async function handleSetup(projectRoot) {
|
|
|
13599
13719
|
`);
|
|
13600
13720
|
continue;
|
|
13601
13721
|
}
|
|
13602
|
-
await runSandboxSetup(sandboxName, projectRoot, config.sandbox.containerWorkdir);
|
|
13722
|
+
await runSandboxSetup(sandboxName, projectRoot, config.sandbox.containerWorkdir, config.sandbox.noSymlinks);
|
|
13603
13723
|
}
|
|
13604
13724
|
}
|
|
13605
13725
|
function buildProviderSandboxNames(projectRoot) {
|
|
@@ -13661,7 +13781,7 @@ async function createProviderSandbox(provider, sandboxName, projectRoot) {
|
|
|
13661
13781
|
}
|
|
13662
13782
|
async function ensurePackageManagerInSandbox(sandboxName, pm) {
|
|
13663
13783
|
try {
|
|
13664
|
-
execSync20(`docker sandbox exec ${sandboxName} which ${pm}`, {
|
|
13784
|
+
execSync20(`docker sandbox exec --privileged ${sandboxName} which ${pm}`, {
|
|
13665
13785
|
stdio: ["pipe", "pipe", "pipe"],
|
|
13666
13786
|
timeout: 5000
|
|
13667
13787
|
});
|
|
@@ -13670,7 +13790,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
|
|
|
13670
13790
|
process.stderr.write(`Installing ${bold2(pm)} in sandbox...
|
|
13671
13791
|
`);
|
|
13672
13792
|
try {
|
|
13673
|
-
execSync20(`docker sandbox exec ${sandboxName} npm install -g ${npmPkg}`, {
|
|
13793
|
+
execSync20(`docker sandbox exec --privileged ${sandboxName} npm install -g ${npmPkg}`, {
|
|
13674
13794
|
stdio: "inherit",
|
|
13675
13795
|
timeout: 120000
|
|
13676
13796
|
});
|
|
@@ -13682,7 +13802,7 @@ async function ensurePackageManagerInSandbox(sandboxName, pm) {
|
|
|
13682
13802
|
}
|
|
13683
13803
|
async function ensureCodexInSandbox(sandboxName) {
|
|
13684
13804
|
try {
|
|
13685
|
-
execSync20(`docker sandbox exec ${sandboxName} which codex`, {
|
|
13805
|
+
execSync20(`docker sandbox exec --privileged ${sandboxName} which codex`, {
|
|
13686
13806
|
stdio: ["pipe", "pipe", "pipe"],
|
|
13687
13807
|
timeout: 5000
|
|
13688
13808
|
});
|
|
@@ -13690,7 +13810,7 @@ async function ensureCodexInSandbox(sandboxName) {
|
|
|
13690
13810
|
process.stderr.write(`Installing codex in sandbox...
|
|
13691
13811
|
`);
|
|
13692
13812
|
try {
|
|
13693
|
-
execSync20(`docker sandbox exec ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
|
|
13813
|
+
execSync20(`docker sandbox exec --privileged ${sandboxName} npm install -g @openai/codex`, { stdio: "inherit", timeout: 120000 });
|
|
13694
13814
|
} catch {
|
|
13695
13815
|
process.stderr.write(`${red2("✗")} Failed to install codex in sandbox.
|
|
13696
13816
|
`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@locusai/cli",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.8",
|
|
4
4
|
"description": "GitHub-native AI engineering assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"license": "MIT",
|
|
37
37
|
"dependencies": {},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@locusai/sdk": "^0.22.
|
|
39
|
+
"@locusai/sdk": "^0.22.8",
|
|
40
40
|
"@types/bun": "latest",
|
|
41
41
|
"typescript": "^5.8.3"
|
|
42
42
|
},
|