@callstack/apex 0.4.3 → 0.4.4

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 +4 -0
  2. package/dist/cli.js +59 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -234,6 +234,10 @@ 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` checks which OpenCode is installed: OpenCode 1 gets `--model`; OpenCode 2,
238
+ whose terminal interface has no `--model`, starts with `--standalone` and the model in
239
+ `OPENCODE_CONFIG_CONTENT`, so the private server sees the model and, for a config that references
240
+ the key variable, your key too.
237
241
 
238
242
  ## Safety and recovery
239
243
 
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { isDeepStrictEqual, parseArgs, styleText } from "node:util";
3
- import { spawn } from "node:child_process";
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,15 @@ async function executable(command, env = process.env, platform = process.platfor
2602
2602
  }
2603
2603
  return null;
2604
2604
  }
2605
+ function openCodeMajor(binary) {
2606
+ if (!binary) return Promise.resolve(1);
2607
+ return new Promise((resolve) => {
2608
+ execFile(binary, ["--version"], { timeout: 1e4 }, (error, stdout) => {
2609
+ const match = !error && /^(?:opencode )?v?(\d+)\.\d+\.\d+\s*$/m.exec(stdout);
2610
+ resolve(match ? Number(match[1]) : 1);
2611
+ });
2612
+ });
2613
+ }
2605
2614
  function locations(home = homedir(), env = process.env, platform = process.platform) {
2606
2615
  const config = env.XDG_CONFIG_HOME || join(home, ".config");
2607
2616
  const userData = platform === "darwin" ? join(home, "Library", "Application Support") : platform === "win32" ? env.APPDATA || join(home, "AppData", "Roaming") : config;
@@ -2837,16 +2846,34 @@ async function planAssistant(assistant, provider = PROVIDERS.vercel) {
2837
2846
  }
2838
2847
  return changes;
2839
2848
  }
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}.`);
2849
+ function launchToken(env, provider) {
2850
+ const token = env[provider.envVar];
2851
+ if (!token?.trim()) throw new Error(`Set ${provider.envVar} in your environment before launching with ${provider.name}.`);
2852
+ return token;
2853
+ }
2854
+ function launchOptions(id, env = process.env, provider = defaultProvider(env), { openCode = 1 } = {}) {
2855
+ const { model } = provider;
2856
+ const token = launchToken(env, provider);
2844
2857
  const childEnv = { ...env };
2845
2858
  switch (id) {
2846
- case "opencode": return {
2847
- args: ["--model", `${provider.opencode}/${model}`],
2848
- env: childEnv
2849
- };
2859
+ case "opencode": {
2860
+ const selected = `${provider.opencode}/${model}`;
2861
+ if (openCode < 2) return {
2862
+ args: ["--model", selected],
2863
+ env: childEnv
2864
+ };
2865
+ const inline = env.OPENCODE_CONFIG_CONTENT?.trim() ? parseJson(env.OPENCODE_CONFIG_CONTENT, "OPENCODE_CONFIG_CONTENT") : {};
2866
+ childEnv.OPENCODE_CONFIG_CONTENT = JSON.stringify({
2867
+ ...inline,
2868
+ model: selected
2869
+ });
2870
+ return {
2871
+ args: ["--standalone"],
2872
+ env: childEnv,
2873
+ afterCommand: true,
2874
+ note: "(OPENCODE_CONFIG_CONTENT model set)"
2875
+ };
2876
+ }
2850
2877
  case "codex": return {
2851
2878
  args: ["--profile", provider.codex],
2852
2879
  env: childEnv
@@ -2884,15 +2911,15 @@ function launchOptions(id, env = process.env, provider = defaultProvider(env)) {
2884
2911
  });
2885
2912
  return {
2886
2913
  args: ["--model", model],
2887
- env: childEnv
2914
+ env: childEnv,
2915
+ note: "(ANTHROPIC_* gateway environment set)"
2888
2916
  };
2889
2917
  default: throw new Error(`Cannot launch ${id}. Use the editor's model selector.`);
2890
2918
  }
2891
2919
  }
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(" ");
2920
+ function runExpansion(id, provider, installed = {}) {
2921
+ const { args, note } = launchOptions(id, { [provider.envVar]: "placeholder" }, provider, installed);
2922
+ return [`${id} ${args.join(" ")}`, note].filter(Boolean).join(" ");
2896
2923
  }
2897
2924
  const copilotModel = (provider) => JSON.stringify({
2898
2925
  id: provider.model,
@@ -4796,6 +4823,7 @@ async function planAll(assistants, options, provider) {
4796
4823
  name: NAMES[assistant.id],
4797
4824
  detected: assistant.detected,
4798
4825
  evidence: assistant.evidence || null,
4826
+ binary: assistant.binary ?? null,
4799
4827
  manual: MANUAL_IDS.includes(assistant.id),
4800
4828
  error: null,
4801
4829
  files: [],
@@ -4970,9 +4998,10 @@ async function commandInit(rest) {
4970
4998
  dim("Apex CLI never reads, stores or logs the key itself.")
4971
4999
  ]);
4972
5000
  const runnable = active.map((entry) => entry.id).filter((id) => RUNNABLE.includes(id));
5001
+ const installed = { openCode: await openCodeMajor(active.find((entry) => entry.id === "opencode")?.binary) };
4973
5002
  const rows = runnable.map((id) => ({
4974
5003
  command: `${apex} run ${id}`,
4975
- expansion: runExpansion(id, provider)
5004
+ expansion: runExpansion(id, provider, installed)
4976
5005
  }));
4977
5006
  const commandWidth = columnWidth(rows.map((row) => row.command));
4978
5007
  section(runnable.length ? `Use these commands to run ${provider.model} with your selected harnesses:` : `Use ${provider.model} with your assistant:`, [
@@ -5139,6 +5168,17 @@ function takeProvider(args) {
5139
5168
  rest: args
5140
5169
  };
5141
5170
  }
5171
+ const isDirectory = (path) => stat(path).then((info) => info.isDirectory(), () => false);
5172
+ async function launchArgs(options, args) {
5173
+ const [first, ...rest] = args;
5174
+ if (options.afterCommand && ["run", "mini"].includes(first)) return [
5175
+ first,
5176
+ ...options.args,
5177
+ ...rest
5178
+ ];
5179
+ if (options.afterCommand && first && !first.startsWith("-") && !await isDirectory(first)) return args;
5180
+ return [...options.args, ...args];
5181
+ }
5142
5182
  async function commandRun(rest) {
5143
5183
  const before = takeProvider(rest);
5144
5184
  const [id, ...after] = before.rest;
@@ -5150,12 +5190,14 @@ async function commandRun(rest) {
5150
5190
  const chosen = before.value ?? behind.value;
5151
5191
  if (chosen !== null && !PROVIDER_IDS.includes(chosen)) throw new Error(`Unknown --provider. Choose from: ${PROVIDER_IDS.join(", ")}`);
5152
5192
  const passed = behind.rest;
5153
- const options = launchOptions(id, process.env, chosen === null ? defaultProvider() : PROVIDERS[chosen]);
5193
+ const provider = chosen === null ? defaultProvider() : PROVIDERS[chosen];
5194
+ launchToken(process.env, provider);
5154
5195
  const binary = await executable(id);
5155
5196
  if (!binary) throw new Error(`${id} was not found on PATH. Install it first, then run ${APEX} init.`);
5156
5197
  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.");
5198
+ const options = launchOptions(id, process.env, provider, { openCode: await openCodeMajor(id === "opencode" && binary) });
5157
5199
  const args = passed[0] === "--" ? passed.slice(1) : passed;
5158
- const child = spawn(binary, [...options.args, ...args], {
5200
+ const child = spawn(binary, await launchArgs(options, args), {
5159
5201
  env: options.env,
5160
5202
  stdio: "inherit",
5161
5203
  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.4",
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": [