@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.
Files changed (2) hide show
  1. package/bin/locus.js +143 -23
  2. 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 + "/.git")} || test -f ${JSON.stringify(candidate + "/package.json")}"`, { stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
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, workdir);
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, noSymlinks) {
13541
+ function getInstallCommand(pm) {
13515
13542
  switch (pm) {
13516
13543
  case "bun":
13517
- return noSymlinks ? ["bun", "install", "--backend=copyfile"] : ["bun", "install"];
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
- async function runSandboxSetup(sandboxName, projectRoot, containerWorkdir) {
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
- const installCmd = getInstallCommand(pm, !!containerWorkdir);
13536
- process.stderr.write(`
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
- const installOk = await runInteractiveCommand("docker", [
13540
- "sandbox",
13541
- "exec",
13542
- "-w",
13543
- workdir,
13544
- sandboxName,
13545
- ...installCmd
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.6",
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.6",
39
+ "@locusai/sdk": "^0.22.8",
40
40
  "@types/bun": "latest",
41
41
  "typescript": "^5.8.3"
42
42
  },