@locusai/cli 0.20.2 → 0.20.3

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 +142 -49
  2. package/package.json +1 -1
package/bin/locus.js CHANGED
@@ -890,7 +890,8 @@ __export(exports_sandbox, {
890
890
  getProviderSandboxName: () => getProviderSandboxName,
891
891
  getModelSandboxName: () => getModelSandboxName,
892
892
  displaySandboxWarning: () => displaySandboxWarning,
893
- detectSandboxSupport: () => detectSandboxSupport
893
+ detectSandboxSupport: () => detectSandboxSupport,
894
+ checkProviderSandboxMismatch: () => checkProviderSandboxMismatch
894
895
  });
895
896
  import { execFile } from "node:child_process";
896
897
  import { createInterface } from "node:readline";
@@ -901,6 +902,19 @@ function getModelSandboxName(config, model, fallbackProvider) {
901
902
  const provider = inferProviderFromModel(model) ?? fallbackProvider;
902
903
  return getProviderSandboxName(config, provider);
903
904
  }
905
+ function checkProviderSandboxMismatch(config, model, fallbackProvider) {
906
+ if (!config.enabled)
907
+ return null;
908
+ const targetProvider = inferProviderFromModel(model) ?? fallbackProvider;
909
+ const sandboxName = getProviderSandboxName(config, targetProvider);
910
+ if (sandboxName)
911
+ return null;
912
+ const configured = ["claude", "codex"].filter((p) => config.providers[p]);
913
+ if (configured.length > 0) {
914
+ return `Sandbox is configured for ${configured.join(", ")} but not for ${targetProvider}. ` + `Run "locus sandbox" and select ${targetProvider} to create its sandbox.`;
915
+ }
916
+ return `No sandbox is configured for ${targetProvider}. ` + `Run "locus sandbox" to create one.`;
917
+ }
904
918
  async function detectSandboxSupport() {
905
919
  if (cachedStatus)
906
920
  return cachedStatus;
@@ -5337,7 +5351,7 @@ ${red("✗")} ${dim("Force exit.")}\r
5337
5351
  return {
5338
5352
  success: false,
5339
5353
  output: "",
5340
- error: `Sandbox for provider "${resolvedProvider}" is not configured. ` + `Run "locus sandbox" and authenticate via "locus sandbox ${resolvedProvider}".`,
5354
+ error: `No sandbox configured for "${resolvedProvider}". ` + `Run "locus sandbox" and select "${resolvedProvider}" to create its sandbox, ` + `then authenticate via "locus sandbox ${resolvedProvider}".`,
5341
5355
  interrupted: false,
5342
5356
  exitCode: 1
5343
5357
  };
@@ -7983,7 +7997,8 @@ async function runInteractiveRepl(session, sessionManager, options) {
7983
7997
  process.stderr.write(`${dim("Using")} ${dim(provider)} ${dim("sandbox")} ${dim(sandboxName)}
7984
7998
  `);
7985
7999
  } else {
7986
- process.stderr.write(`${yellow("⚠")} ${dim(`No sandbox configured for ${provider}. Run locus sandbox.`)}
8000
+ const mismatch = checkProviderSandboxMismatch(config.sandbox, config.ai.model, config.ai.provider);
8001
+ process.stderr.write(`${yellow("⚠")} ${dim(mismatch ?? `No sandbox configured for ${provider}. Run locus sandbox.`)}
7987
8002
  `);
7988
8003
  }
7989
8004
  }
@@ -8027,7 +8042,8 @@ async function runInteractiveRepl(session, sessionManager, options) {
8027
8042
  `);
8028
8043
  } else {
8029
8044
  sandboxRunner = null;
8030
- process.stderr.write(`${yellow("⚠")} ${dim(`No sandbox configured for ${inferredProvider}. Run locus sandbox.`)}
8045
+ const mismatch = checkProviderSandboxMismatch(config.sandbox, model, config.ai.provider);
8046
+ process.stderr.write(`${yellow("⚠")} ${dim(mismatch ?? `No sandbox configured for ${inferredProvider}. Run locus sandbox.`)}
8031
8047
  `);
8032
8048
  }
8033
8049
  }
@@ -8332,7 +8348,8 @@ async function handleJsonStream(projectRoot, config, args, sessionId) {
8332
8348
  const sandboxName = getProviderSandboxName(config.sandbox, config.ai.provider);
8333
8349
  const runner = config.sandbox.enabled ? sandboxName ? createUserManagedSandboxRunner(config.ai.provider, sandboxName) : null : await createRunnerAsync(config.ai.provider, false);
8334
8350
  if (!runner) {
8335
- stream.emitError(`Sandbox for provider "${config.ai.provider}" is not configured. Run locus sandbox.`, false);
8351
+ const mismatch = checkProviderSandboxMismatch(config.sandbox, config.ai.model, config.ai.provider);
8352
+ stream.emitError(mismatch ?? `No sandbox configured for "${config.ai.provider}". Run "locus sandbox" to create one.`, false);
8336
8353
  return;
8337
8354
  }
8338
8355
  const available = await runner.isAvailable();
@@ -9335,6 +9352,15 @@ async function runCommand(projectRoot, args, flags = {}) {
9335
9352
  process.stderr.write(`${yellow("⚠")} Running without sandbox. The AI agent will have unrestricted access to your filesystem, network, and environment variables.
9336
9353
  `);
9337
9354
  }
9355
+ if (sandboxed) {
9356
+ const model = flags.model ?? config.ai.model;
9357
+ const mismatch = checkProviderSandboxMismatch(config.sandbox, model, config.ai.provider);
9358
+ if (mismatch) {
9359
+ process.stderr.write(`${red("✗")} ${mismatch}
9360
+ `);
9361
+ return;
9362
+ }
9363
+ }
9338
9364
  if (flags.resume) {
9339
9365
  return handleResume(projectRoot, config, sandboxed);
9340
9366
  }
@@ -10288,6 +10314,14 @@ ${bold("Planning:")} ${cyan(displayDirective)}
10288
10314
  }
10289
10315
  process.stderr.write(`
10290
10316
  `);
10317
+ if (config.sandbox.enabled) {
10318
+ const mismatch = checkProviderSandboxMismatch(config.sandbox, flags.model ?? config.ai.model, config.ai.provider);
10319
+ if (mismatch) {
10320
+ process.stderr.write(`${red("✗")} ${mismatch}
10321
+ `);
10322
+ return;
10323
+ }
10324
+ }
10291
10325
  const prompt = buildPlanningPrompt(projectRoot, config, directive, sprintName, id, planPathRelative);
10292
10326
  const aiResult = await runAI({
10293
10327
  prompt,
@@ -10383,6 +10417,14 @@ ${bold("Organizing issues for:")} ${cyan(sprintName)}
10383
10417
  `);
10384
10418
  return;
10385
10419
  }
10420
+ if (config.sandbox.enabled) {
10421
+ const mismatch = checkProviderSandboxMismatch(config.sandbox, flags.model ?? config.ai.model, config.ai.provider);
10422
+ if (mismatch) {
10423
+ process.stderr.write(`${red("✗")} ${mismatch}
10424
+ `);
10425
+ return;
10426
+ }
10427
+ }
10386
10428
  const issueDescriptions = issues.map((i) => `#${i.number}: ${i.title}
10387
10429
  ${i.body?.slice(0, 300) ?? ""}`).join(`
10388
10430
 
@@ -10712,6 +10754,14 @@ async function reviewCommand(projectRoot, args, flags = {}) {
10712
10754
  prNumber = Number.parseInt(args[i], 10);
10713
10755
  }
10714
10756
  }
10757
+ if (config.sandbox.enabled) {
10758
+ const mismatch = checkProviderSandboxMismatch(config.sandbox, flags.model ?? config.ai.model, config.ai.provider);
10759
+ if (mismatch) {
10760
+ process.stderr.write(`${red("✗")} ${mismatch}
10761
+ `);
10762
+ return;
10763
+ }
10764
+ }
10715
10765
  if (prNumber) {
10716
10766
  return reviewSinglePR(projectRoot, config, prNumber, focus, flags);
10717
10767
  }
@@ -10939,6 +10989,14 @@ async function iterateCommand(projectRoot, args, flags = {}) {
10939
10989
  issueNumber = Number.parseInt(args[i], 10);
10940
10990
  }
10941
10991
  }
10992
+ if (config.sandbox.enabled) {
10993
+ const mismatch = checkProviderSandboxMismatch(config.sandbox, config.ai.model, config.ai.provider);
10994
+ if (mismatch) {
10995
+ process.stderr.write(`${red("✗")} ${mismatch}
10996
+ `);
10997
+ return;
10998
+ }
10999
+ }
10942
11000
  if (prNumber) {
10943
11001
  return handleSinglePR(projectRoot, config, prNumber, flags);
10944
11002
  }
@@ -11135,6 +11193,7 @@ var init_iterate = __esm(() => {
11135
11193
  init_agent();
11136
11194
  init_config();
11137
11195
  init_github();
11196
+ init_sandbox();
11138
11197
  init_progress();
11139
11198
  init_terminal();
11140
11199
  });
@@ -11339,6 +11398,14 @@ async function startDiscussion(projectRoot, topic, flags) {
11339
11398
  const config = loadConfig(projectRoot);
11340
11399
  const timer = createTimer();
11341
11400
  const id = generateId2();
11401
+ if (config.sandbox.enabled) {
11402
+ const mismatch = checkProviderSandboxMismatch(config.sandbox, flags.model ?? config.ai.model, config.ai.provider);
11403
+ if (mismatch) {
11404
+ process.stderr.write(`${red("✗")} ${mismatch}
11405
+ `);
11406
+ return;
11407
+ }
11408
+ }
11342
11409
  process.stderr.write(`
11343
11410
  ${bold("Discussion:")} ${cyan(topic)}
11344
11411
 
@@ -11733,12 +11800,13 @@ import { execSync as execSync17, spawn as spawn6 } from "node:child_process";
11733
11800
  import { createHash } from "node:crypto";
11734
11801
  import { existsSync as existsSync24, readFileSync as readFileSync17 } from "node:fs";
11735
11802
  import { basename as basename4, join as join24 } from "node:path";
11803
+ import { createInterface as createInterface3 } from "node:readline";
11736
11804
  function printSandboxHelp() {
11737
11805
  process.stderr.write(`
11738
11806
  ${bold("locus sandbox")} — Manage Docker sandbox lifecycle
11739
11807
 
11740
11808
  ${bold("Usage:")}
11741
- locus sandbox ${dim("# Create claude/codex sandboxes and enable sandbox mode")}
11809
+ locus sandbox ${dim("# Select a provider and create its sandbox")}
11742
11810
  locus sandbox claude ${dim("# Run claude interactively (for login)")}
11743
11811
  locus sandbox codex ${dim("# Run codex interactively (for login)")}
11744
11812
  locus sandbox setup ${dim("# Re-run dependency install in sandbox(es)")}
@@ -11749,10 +11817,10 @@ ${bold("Usage:")}
11749
11817
  locus sandbox status ${dim("# Show current sandbox state")}
11750
11818
 
11751
11819
  ${bold("Flow:")}
11752
- 1. ${cyan("locus sandbox")} Create sandboxes (auto-installs dependencies)
11753
- 2. ${cyan("locus sandbox claude")} Login Claude inside its sandbox
11754
- 3. ${cyan("locus sandbox codex")} Login Codex inside its sandbox
11755
- 4. ${cyan("locus sandbox install bun")} Install extra tools (optional)
11820
+ 1. ${cyan("locus sandbox")} Select a provider and create its sandbox
11821
+ 2. ${cyan("locus sandbox <provider>")} Login to the provider inside its sandbox
11822
+ 3. ${cyan("locus sandbox install bun")} Install extra tools (optional)
11823
+ 4. Run ${cyan("locus sandbox")} again to add another provider (optional)
11756
11824
 
11757
11825
  `);
11758
11826
  }
@@ -11786,6 +11854,41 @@ async function sandboxCommand(projectRoot, args) {
11786
11854
  `);
11787
11855
  }
11788
11856
  }
11857
+ async function promptProviderSelection() {
11858
+ process.stderr.write(`
11859
+ ${bold("Select a provider to create a sandbox for:")}
11860
+
11861
+ `);
11862
+ for (let i = 0;i < PROVIDERS.length; i++) {
11863
+ process.stderr.write(` ${bold(String(i + 1))}. ${PROVIDERS[i]}
11864
+ `);
11865
+ }
11866
+ process.stderr.write(`
11867
+ `);
11868
+ const rl = createInterface3({
11869
+ input: process.stdin,
11870
+ output: process.stderr
11871
+ });
11872
+ return new Promise((resolve2) => {
11873
+ rl.question("Enter choice (1-2): ", (answer) => {
11874
+ rl.close();
11875
+ const trimmed = answer.trim().toLowerCase();
11876
+ if (trimmed === "claude" || trimmed === "codex") {
11877
+ resolve2(trimmed);
11878
+ return;
11879
+ }
11880
+ const num = Number.parseInt(trimmed, 10);
11881
+ if (num >= 1 && num <= PROVIDERS.length) {
11882
+ resolve2(PROVIDERS[num - 1]);
11883
+ return;
11884
+ }
11885
+ process.stderr.write(`${red("✗")} Invalid selection.
11886
+ `);
11887
+ resolve2(null);
11888
+ });
11889
+ rl.on("close", () => resolve2(null));
11890
+ });
11891
+ }
11789
11892
  async function handleCreate(projectRoot) {
11790
11893
  const config = loadConfig(projectRoot);
11791
11894
  const status = await detectSandboxSupport();
@@ -11796,54 +11899,44 @@ async function handleCreate(projectRoot) {
11796
11899
  `);
11797
11900
  return;
11798
11901
  }
11902
+ const provider = await promptProviderSelection();
11903
+ if (!provider)
11904
+ return;
11799
11905
  const sandboxNames = buildProviderSandboxNames(projectRoot);
11800
- const readySandboxes = {};
11801
- const newlyCreated = new Set;
11802
- let failed = false;
11803
- const createResults = await Promise.all(PROVIDERS.map(async (provider) => {
11804
- const name = sandboxNames[provider];
11805
- if (isSandboxAlive(name)) {
11806
- process.stderr.write(`${green("✓")} ${provider} sandbox ready: ${bold(name)}
11906
+ const name = sandboxNames[provider];
11907
+ const readySandboxes = { ...config.sandbox.providers };
11908
+ if (isSandboxAlive(name)) {
11909
+ process.stderr.write(`${green("✓")} ${provider} sandbox already exists: ${bold(name)}
11910
+ `);
11911
+ readySandboxes[provider] = name;
11912
+ config.sandbox.enabled = true;
11913
+ config.sandbox.providers = readySandboxes;
11914
+ saveConfig(projectRoot, config);
11915
+ process.stderr.write(` Next: run ${cyan(`locus sandbox ${provider}`)} to authenticate.
11807
11916
  `);
11808
- return { provider, name, created: false, existed: true };
11809
- }
11810
- process.stderr.write(`Creating ${bold(provider)} sandbox ${dim(name)} with workspace ${dim(projectRoot)}...
11917
+ return;
11918
+ }
11919
+ process.stderr.write(`Creating ${bold(provider)} sandbox ${dim(name)} with workspace ${dim(projectRoot)}...
11811
11920
  `);
11812
- const created = await createProviderSandbox(provider, name, projectRoot);
11813
- if (!created) {
11814
- process.stderr.write(`${red("✗")} Failed to create ${provider} sandbox (${name}).
11921
+ const created = await createProviderSandbox(provider, name, projectRoot);
11922
+ if (!created) {
11923
+ process.stderr.write(`${red("✗")} Failed to create ${provider} sandbox (${name}).
11815
11924
  `);
11816
- return { provider, name, created: false, existed: false };
11817
- }
11818
- process.stderr.write(`${green("✓")} ${provider} sandbox created: ${bold(name)}
11925
+ process.stderr.write(` Re-run ${cyan("locus sandbox")} after resolving Docker issues.
11819
11926
  `);
11820
- return { provider, name, created: true, existed: false };
11821
- }));
11822
- for (const result of createResults) {
11823
- if (result.created || result.existed) {
11824
- readySandboxes[result.provider] = result.name;
11825
- if (result.created)
11826
- newlyCreated.add(result.name);
11827
- } else {
11828
- failed = true;
11829
- }
11927
+ return;
11830
11928
  }
11929
+ process.stderr.write(`${green("✓")} ${provider} sandbox created: ${bold(name)}
11930
+ `);
11931
+ readySandboxes[provider] = name;
11831
11932
  config.sandbox.enabled = true;
11832
11933
  config.sandbox.providers = readySandboxes;
11833
11934
  saveConfig(projectRoot, config);
11834
- await Promise.all(PROVIDERS.filter((provider) => {
11835
- const sandboxName = readySandboxes[provider];
11836
- return sandboxName && newlyCreated.has(sandboxName);
11837
- }).map((provider) => runSandboxSetup(readySandboxes[provider], projectRoot)));
11838
- if (failed) {
11839
- process.stderr.write(`
11840
- ${yellow("⚠")} Some sandboxes failed to create. Re-run ${cyan("locus sandbox")} after resolving Docker issues.
11841
- `);
11842
- }
11935
+ await runSandboxSetup(name, projectRoot);
11843
11936
  process.stderr.write(`
11844
- ${green("✓")} Sandbox mode enabled with provider-specific sandboxes.
11937
+ ${green("✓")} Sandbox mode enabled for ${bold(provider)}.
11845
11938
  `);
11846
- process.stderr.write(` Next: run ${cyan("locus sandbox claude")} and ${cyan("locus sandbox codex")} to authenticate both providers.
11939
+ process.stderr.write(` Next: run ${cyan(`locus sandbox ${provider}`)} to authenticate.
11847
11940
  `);
11848
11941
  }
11849
11942
  async function handleAgentLogin(projectRoot, agent) {
@@ -11939,9 +12032,9 @@ ${bold("Sandbox Status")}
11939
12032
  `);
11940
12033
  }
11941
12034
  }
11942
- if (!config.sandbox.providers.claude || !config.sandbox.providers.codex) {
12035
+ if (!config.sandbox.providers.claude && !config.sandbox.providers.codex) {
11943
12036
  process.stderr.write(`
11944
- ${yellow("⚠")} Provider sandboxes are incomplete. Run ${bold("locus sandbox")}.
12037
+ ${yellow("⚠")} No provider sandboxes configured. Run ${bold("locus sandbox")} to create one.
11945
12038
  `);
11946
12039
  }
11947
12040
  process.stderr.write(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@locusai/cli",
3
- "version": "0.20.2",
3
+ "version": "0.20.3",
4
4
  "description": "GitHub-native AI engineering assistant",
5
5
  "type": "module",
6
6
  "bin": {