@tagma/sdk 0.5.2 → 0.6.1

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.
@@ -33,17 +33,25 @@ const EFFORT_TO_VARIANT: Record<string, string | null> = {
33
33
 
34
34
  // ── Auto-install + free-model picker ───────────────────────────────────────
35
35
  //
36
- // The opencode driver is SDK-built-in, but the `opencode` CLI isn't; we
37
- // auto-install it on demand (via `bun install -g opencode-ai`) and pick a
38
- // sensible default model from whatever the CLI reports. Both checks are
39
- // process-cached via module-level variables so each concern runs at most
40
- // once per SDK process.
36
+ // The opencode driver is SDK-built-in, but the `opencode` CLI isn't. Two
37
+ // provisioning paths:
41
38
  //
42
- // Design:
43
- // - User-provided `model:` wins; we only compute a default when it's empty.
44
- // - Failure modes never throw — they fall back to `DEFAULT_MODEL` and let
45
- // the subsequent `opencode run` spawn fail with its own error. Avoids
46
- // two confusing errors for one missing dependency.
39
+ // 1. Desktop app — the Electron shell ships a platform-matched opencode
40
+ // binary under resources/opencode/bin/, prepended to the sidecar's PATH
41
+ // at launch (see packages/electron/src/runtime-paths.ts). In-app updates
42
+ // drop a newer copy into userData/opencode/bin/ which wins via PATH
43
+ // precedence. That path resolves on the first `opencode --version`
44
+ // probe below; no auto-install ever fires.
45
+ //
46
+ // 2. SDK direct use — when bun is on PATH we fall through to
47
+ // `bun install -g opencode-ai`, identical to the pre-desktop behavior.
48
+ //
49
+ // When BOTH paths are unavailable (no bundled binary, no bun) we fail with
50
+ // an actionable error pointing at the desktop Settings panel instead of
51
+ // silently letting `opencode run` ENOENT later — the old behavior swallowed
52
+ // the root cause in runCapture's catch and left the user staring at an
53
+ // opaque "exit code -1". The result is process-memoized so subsequent
54
+ // tasks in the same run surface the same error without re-probing.
47
55
 
48
56
  interface OpencodeModelInfo {
49
57
  id?: string;
@@ -53,7 +61,12 @@ interface OpencodeModelInfo {
53
61
  limit?: { context?: number };
54
62
  }
55
63
 
64
+ // Memoize BOTH success and failure. On failure we stash the message so every
65
+ // subsequent ensureOpencodeInstalled() throws the identical error — re-running
66
+ // the bun-install probe for each task of a failed run would just be slow and
67
+ // produce confusing interleaved stderr.
56
68
  let opencodeReady: boolean | undefined;
69
+ let opencodeReadyError: string | undefined;
57
70
  let cachedDefaultModel: string | undefined;
58
71
 
59
72
  async function runCapture(
@@ -72,14 +85,38 @@ async function runCapture(
72
85
  }
73
86
  }
74
87
 
75
- async function ensureOpencodeInstalled(): Promise<boolean> {
76
- if (opencodeReady !== undefined) return opencodeReady;
88
+ // Shared tail for every failure message — the Tagma desktop app exposes a
89
+ // one-click installer at the same npm source path this driver would reach
90
+ // for, so point users there first. Users running the SDK as a library still
91
+ // see the manual bun/npm hint.
92
+ const SETUP_HINT =
93
+ 'If you are using the Tagma desktop app, open Editor Settings → OpenCode CLI to install or update the bundled binary. ' +
94
+ 'Otherwise install it manually: `bun install -g opencode-ai` or `npm install -g opencode-ai`.';
95
+
96
+ async function ensureOpencodeInstalled(): Promise<void> {
97
+ if (opencodeReady === true) return;
98
+ if (opencodeReady === false && opencodeReadyError) {
99
+ throw new Error(opencodeReadyError);
100
+ }
77
101
 
78
- // Probe existing install first — users who already have it get no delay.
102
+ // Probe existing install first — this is the hot path for desktop users
103
+ // (bundled binary in PATH) and for anyone who already has opencode.
79
104
  const probe = await runCapture(['opencode', '--version']);
80
105
  if (probe.code === 0) {
81
106
  opencodeReady = true;
82
- return true;
107
+ return;
108
+ }
109
+
110
+ // Distinguish "bun is missing" from "bun is here but install failed" so
111
+ // the error we surface points at the right next step. If bun is absent we
112
+ // skip the install attempt entirely — spawning with `bun` as argv[0]
113
+ // would just ENOENT inside runCapture's catch and look identical to a
114
+ // failed install.
115
+ const bunProbe = await runCapture(['bun', '--version']);
116
+ if (bunProbe.code !== 0) {
117
+ opencodeReady = false;
118
+ opencodeReadyError = `OpenCode CLI is not available and \`bun\` is not installed. ${SETUP_HINT}`;
119
+ throw new Error(opencodeReadyError);
83
120
  }
84
121
 
85
122
  console.error(
@@ -93,9 +130,9 @@ async function ensureOpencodeInstalled(): Promise<boolean> {
93
130
  });
94
131
  const installCode = await install.exited;
95
132
  if (installCode !== 0) {
96
- console.error('[driver:opencode] install failed — opencode run will likely fail below.');
97
133
  opencodeReady = false;
98
- return false;
134
+ opencodeReadyError = `\`bun install -g opencode-ai\` failed (exit code ${installCode}). ${SETUP_HINT}`;
135
+ throw new Error(opencodeReadyError);
99
136
  }
100
137
 
101
138
  // Bun installs globals under `~/.bun/bin` (or `%USERPROFILE%\.bun\bin`),
@@ -113,13 +150,14 @@ async function ensureOpencodeInstalled(): Promise<boolean> {
113
150
  }
114
151
 
115
152
  const verify = await runCapture(['opencode', '--version']);
116
- opencodeReady = verify.code === 0;
117
- if (!opencodeReady) {
118
- console.error(
119
- '[driver:opencode] `opencode` still not resolvable after install — check that bun global bin is on PATH.',
120
- );
153
+ if (verify.code !== 0) {
154
+ opencodeReady = false;
155
+ opencodeReadyError =
156
+ '`opencode` is not resolvable after `bun install -g opencode-ai` completed. ' +
157
+ "Bun's global bin directory is probably not on PATH — add it manually or restart the app.";
158
+ throw new Error(opencodeReadyError);
121
159
  }
122
- return opencodeReady;
160
+ opencodeReady = true;
123
161
  }
124
162
 
125
163
  // `opencode models --verbose` emits "<provider>/<id>\n{...json...}\n" pairs.
@@ -174,11 +212,11 @@ function pickFreeModel(models: OpencodeModelInfo[]): string | null {
174
212
 
175
213
  async function resolveDefaultModel(): Promise<string> {
176
214
  if (cachedDefaultModel !== undefined) return cachedDefaultModel;
177
- const ready = await ensureOpencodeInstalled();
178
- if (!ready) {
179
- cachedDefaultModel = DEFAULT_MODEL;
180
- return cachedDefaultModel;
181
- }
215
+ // ensureOpencodeInstalled now throws with an actionable message when the
216
+ // CLI can't be provisioned, so we let the error bubble up to the task
217
+ // runner instead of silently falling back to DEFAULT_MODEL (which would
218
+ // produce a second confusing ENOENT a few lines later in `opencode run`).
219
+ await ensureOpencodeInstalled();
182
220
  console.error('[driver:opencode] resolving free opencode model...');
183
221
  const { code, stdout } = await runCapture(['opencode', 'models', '--verbose']);
184
222
  if (code !== 0) {
@@ -207,8 +245,9 @@ export const OpenCodeDriver: DriverPlugin = {
207
245
  async buildCommand(task: TaskConfig, track: TrackConfig, ctx: DriverContext): Promise<SpawnSpec> {
208
246
  const explicitModel = task.model ?? track.model;
209
247
  // Always make sure the opencode CLI is usable before we spawn it — even
210
- // when the user pinned a model. If missing, ensureOpencodeInstalled
211
- // auto-installs it via `bun install -g opencode-ai`.
248
+ // when the user pinned a model. ensureOpencodeInstalled throws with an
249
+ // actionable message when the binary is neither present on PATH (desktop
250
+ // bundles it there via runtime-paths.ts) nor installable via bun.
212
251
  if (explicitModel) await ensureOpencodeInstalled();
213
252
  // Otherwise resolveDefaultModel both ensures the CLI and picks a free
214
253
  // model from `opencode models --verbose` (cached per-process).