@callstack/apex 0.4.3 → 0.4.5

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 (3) hide show
  1. package/README.md +6 -0
  2. package/dist/cli.js +71 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -234,6 +234,12 @@ Other launchers: `apex run opencode`, `apex run claude`, `apex run pi`, `apex ru
234
234
  Launchers pass extra arguments through unchanged (without a shell), can be overridden by explicit
235
235
  user arguments, and never run `init`. `apex run claude` sets the gateway URL, token, attribution
236
236
  flag and model aliases in the child process only, leaving your default Claude provider alone.
237
+ `apex run opencode` asks the installed binary which interface it has (`--standalone` exists only on
238
+ OpenCode 2; its version number says nothing, because dev and beta builds of both generations report
239
+ `0.0.0-*`): OpenCode 1 gets `--model`; OpenCode 2, whose terminal interface has no `--model`, starts
240
+ with `--standalone` and the model in `OPENCODE_CONFIG_CONTENT`, so the private server sees the
241
+ model and, for a config that references the key variable, your key too. A binary that cannot be
242
+ asked launches with neither flag, on your own configuration, and says so.
237
243
 
238
244
  ## Safety and recovery
239
245
 
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { isDeepStrictEqual, parseArgs, styleText } from "node:util";
3
- import { spawn } from "node:child_process";
2
+ import { isDeepStrictEqual, parseArgs, promisify, styleText } from "node:util";
3
+ import { execFile, spawn } from "node:child_process";
4
4
  import { access, lstat, mkdir, open, readFile, rename, stat, unlink } from "node:fs/promises";
5
5
  import { basename, delimiter, dirname, join, parse, resolve } from "node:path";
6
6
  import { createHash, randomUUID } from "node:crypto";
@@ -2602,6 +2602,16 @@ async function executable(command, env = process.env, platform = process.platfor
2602
2602
  }
2603
2603
  return null;
2604
2604
  }
2605
+ const execFileAsync = promisify(execFile);
2606
+ async function openCodeSupportsStandalone(binary) {
2607
+ if (!binary) return null;
2608
+ try {
2609
+ const { stdout, stderr } = await execFileAsync(binary, ["--help"], { timeout: 5e3 });
2610
+ return `${stdout}${stderr}`.includes("--standalone");
2611
+ } catch (error) {
2612
+ return `${error.stdout}${error.stderr}`.includes("--standalone") ? true : null;
2613
+ }
2614
+ }
2605
2615
  function locations(home = homedir(), env = process.env, platform = process.platform) {
2606
2616
  const config = env.XDG_CONFIG_HOME || join(home, ".config");
2607
2617
  const userData = platform === "darwin" ? join(home, "Library", "Application Support") : platform === "win32" ? env.APPDATA || join(home, "AppData", "Roaming") : config;
@@ -2837,16 +2847,40 @@ async function planAssistant(assistant, provider = PROVIDERS.vercel) {
2837
2847
  }
2838
2848
  return changes;
2839
2849
  }
2840
- function launchOptions(id, env = process.env, provider = defaultProvider(env)) {
2841
- const { model, envVar } = provider;
2842
- const token = env[envVar];
2843
- if (!token?.trim()) throw new Error(`Set ${envVar} in your environment before launching with ${provider.name}.`);
2850
+ function launchToken(env, provider) {
2851
+ const token = env[provider.envVar];
2852
+ if (!token?.trim()) throw new Error(`Set ${provider.envVar} in your environment before launching with ${provider.name}.`);
2853
+ return token;
2854
+ }
2855
+ function launchOptions(id, env = process.env, provider = defaultProvider(env), { openCodeStandalone = false } = {}) {
2856
+ const { model } = provider;
2857
+ const token = launchToken(env, provider);
2844
2858
  const childEnv = { ...env };
2845
2859
  switch (id) {
2846
- case "opencode": return {
2847
- args: ["--model", `${provider.opencode}/${model}`],
2848
- env: childEnv
2849
- };
2860
+ case "opencode": {
2861
+ const selected = `${provider.opencode}/${model}`;
2862
+ if (openCodeStandalone === null) return {
2863
+ args: [],
2864
+ env: childEnv,
2865
+ note: "(OpenCode interface unknown: no model flag added)",
2866
+ warning: "Apex CLI: Could not ask which OpenCode interface is installed, so it launches without a model flag. OpenCode 1 takes --model, OpenCode 2 takes --standalone; opencode --help says which you have."
2867
+ };
2868
+ if (!openCodeStandalone) return {
2869
+ args: ["--model", selected],
2870
+ env: childEnv
2871
+ };
2872
+ const inline = env.OPENCODE_CONFIG_CONTENT?.trim() ? parseJson(env.OPENCODE_CONFIG_CONTENT, "OPENCODE_CONFIG_CONTENT") : {};
2873
+ childEnv.OPENCODE_CONFIG_CONTENT = JSON.stringify({
2874
+ ...inline,
2875
+ model: selected
2876
+ });
2877
+ return {
2878
+ args: ["--standalone"],
2879
+ env: childEnv,
2880
+ afterCommand: true,
2881
+ note: "(OPENCODE_CONFIG_CONTENT model set)"
2882
+ };
2883
+ }
2850
2884
  case "codex": return {
2851
2885
  args: ["--profile", provider.codex],
2852
2886
  env: childEnv
@@ -2884,15 +2918,15 @@ function launchOptions(id, env = process.env, provider = defaultProvider(env)) {
2884
2918
  });
2885
2919
  return {
2886
2920
  args: ["--model", model],
2887
- env: childEnv
2921
+ env: childEnv,
2922
+ note: "(ANTHROPIC_* gateway environment set)"
2888
2923
  };
2889
2924
  default: throw new Error(`Cannot launch ${id}. Use the editor's model selector.`);
2890
2925
  }
2891
2926
  }
2892
- const RUN_NOTE = { claude: "(ANTHROPIC_* gateway environment set)" };
2893
- function runExpansion(id, provider) {
2894
- const { args } = launchOptions(id, { [provider.envVar]: "placeholder" }, provider);
2895
- return [`${id} ${args.join(" ")}`, RUN_NOTE[id]].filter(Boolean).join(" ");
2927
+ function runExpansion(id, provider, installed = {}) {
2928
+ const { args, note } = launchOptions(id, { [provider.envVar]: "placeholder" }, provider, installed);
2929
+ return [[id, ...args].join(" "), note].filter(Boolean).join(" ");
2896
2930
  }
2897
2931
  const copilotModel = (provider) => JSON.stringify({
2898
2932
  id: provider.model,
@@ -4796,6 +4830,7 @@ async function planAll(assistants, options, provider) {
4796
4830
  name: NAMES[assistant.id],
4797
4831
  detected: assistant.detected,
4798
4832
  evidence: assistant.evidence || null,
4833
+ binary: assistant.binary ?? null,
4799
4834
  manual: MANUAL_IDS.includes(assistant.id),
4800
4835
  error: null,
4801
4836
  files: [],
@@ -4970,9 +5005,12 @@ async function commandInit(rest) {
4970
5005
  dim("Apex CLI never reads, stores or logs the key itself.")
4971
5006
  ]);
4972
5007
  const runnable = active.map((entry) => entry.id).filter((id) => RUNNABLE.includes(id));
5008
+ const opencodeBinary = active.find((entry) => entry.id === "opencode")?.binary;
5009
+ const probed = opencodeBinary ? await openCodeSupportsStandalone(opencodeBinary) : void 0;
5010
+ const installed = { openCodeStandalone: probed };
4973
5011
  const rows = runnable.map((id) => ({
4974
5012
  command: `${apex} run ${id}`,
4975
- expansion: runExpansion(id, provider)
5013
+ expansion: id === "opencode" && probed === void 0 ? id : runExpansion(id, provider, installed)
4976
5014
  }));
4977
5015
  const commandWidth = columnWidth(rows.map((row) => row.command));
4978
5016
  section(runnable.length ? `Use these commands to run ${provider.model} with your selected harnesses:` : `Use ${provider.model} with your assistant:`, [
@@ -5139,6 +5177,17 @@ function takeProvider(args) {
5139
5177
  rest: args
5140
5178
  };
5141
5179
  }
5180
+ const isDirectory = (path) => stat(path).then((info) => info.isDirectory(), () => false);
5181
+ async function launchArgs(options, args) {
5182
+ const [first, ...rest] = args;
5183
+ if (options.afterCommand && ["run", "mini"].includes(first)) return [
5184
+ first,
5185
+ ...options.args,
5186
+ ...rest
5187
+ ];
5188
+ if (options.afterCommand && first && !first.startsWith("-") && !await isDirectory(first)) return args;
5189
+ return [...options.args, ...args];
5190
+ }
5142
5191
  async function commandRun(rest) {
5143
5192
  const before = takeProvider(rest);
5144
5193
  const [id, ...after] = before.rest;
@@ -5150,12 +5199,16 @@ async function commandRun(rest) {
5150
5199
  const chosen = before.value ?? behind.value;
5151
5200
  if (chosen !== null && !PROVIDER_IDS.includes(chosen)) throw new Error(`Unknown --provider. Choose from: ${PROVIDER_IDS.join(", ")}`);
5152
5201
  const passed = behind.rest;
5153
- const options = launchOptions(id, process.env, chosen === null ? defaultProvider() : PROVIDERS[chosen]);
5202
+ const provider = chosen === null ? defaultProvider() : PROVIDERS[chosen];
5203
+ launchToken(process.env, provider);
5154
5204
  const binary = await executable(id);
5155
5205
  if (!binary) throw new Error(`${id} was not found on PATH. Install it first, then run ${APEX} init.`);
5156
5206
  if (process.platform === "win32" && /\.(cmd|bat)$/i.test(binary)) throw new Error("Windows .cmd/.bat launchers are not executed through a shell. Use WSL or launch the configured assistant directly.");
5207
+ const standalone = id === "opencode" ? await openCodeSupportsStandalone(binary) : false;
5208
+ const options = launchOptions(id, process.env, provider, { openCodeStandalone: standalone });
5209
+ if (options.warning) console.error(styleText("yellow", options.warning));
5157
5210
  const args = passed[0] === "--" ? passed.slice(1) : passed;
5158
- const child = spawn(binary, [...options.args, ...args], {
5211
+ const child = spawn(binary, await launchArgs(options, args), {
5159
5212
  env: options.env,
5160
5213
  stdio: "inherit",
5161
5214
  shell: false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@callstack/apex",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Detects the AI coding assistants installed on your machine and configures them to use Callstack Apex.",
5
5
  "license": "MIT",
6
6
  "keywords": [