ework-web 0.1.3 → 0.1.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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/opencode.ts +38 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-web",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "ework-web — standalone multi-project issue tracker. Local SQLite-backed, no external API dependency. Bun + TypeScript + SSR HTML.",
6
6
  "license": "MIT",
package/src/opencode.ts CHANGED
@@ -170,11 +170,16 @@ export class OpencodeClient {
170
170
  }
171
171
  const text = stdout.trim();
172
172
  if (!text) return null;
173
- try {
174
- return JSON.parse(text);
175
- } catch {
173
+ const jsonText = stripNonJsonPreamble(text);
174
+ if (jsonText === null) {
176
175
  throw new OpencodeError(`opencode ${args.join(" ")}: non-JSON output`, 502);
177
176
  }
177
+ try {
178
+ return JSON.parse(jsonText);
179
+ } catch (e) {
180
+ const msg = e instanceof Error ? e.message : String(e);
181
+ throw new OpencodeError(`opencode ${args.join(" ")}: malformed JSON (${msg})`, 502);
182
+ }
178
183
  }
179
184
 
180
185
  private async run(args: string[]): Promise<{ stdout: string; stderr: string; code: number | null }> {
@@ -239,6 +244,36 @@ function asString(v: unknown): string {
239
244
  return typeof v === "string" ? v : "";
240
245
  }
241
246
 
247
+ // `opencode export` (and other opencode subcommands that emit JSON) write
248
+ // status lines to stdout BEFORE the JSON payload:
249
+ // • plugin banners via console.log: "[omo-stable] Downloading comment-checker binary..."
250
+ // • plugin registration via console.log: "[omo-stable] ast-grep binary ready."
251
+ // • CLI status: "Exporting session: ses_..."
252
+ // • any other stdout-side logging from plugins
253
+ // These break JSON.parse(). Find the first line that starts a JSON object
254
+ // or array and slice from there. Returns null if no JSON-looking content.
255
+ //
256
+ // We try JSON.parse at each candidate (line starting with '{' or '[') and
257
+ // return the first one that parses — necessary because plugin banners like
258
+ // "[omo-stable] ..." also start with '[' but aren't JSON.
259
+ export function stripNonJsonPreamble(s: string): string | null {
260
+ const lines = s.split(/\r?\n/);
261
+ for (let i = 0; i < lines.length; i++) {
262
+ const line = lines[i];
263
+ if (!line) continue;
264
+ const trimmed = line.trimStart();
265
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) continue;
266
+ const candidate = lines.slice(i).join("\n");
267
+ try {
268
+ JSON.parse(candidate);
269
+ return candidate;
270
+ } catch {
271
+ continue;
272
+ }
273
+ }
274
+ return null;
275
+ }
276
+
242
277
  function parseSessionExport(v: unknown): SessionExport | null {
243
278
  if (!v || typeof v !== "object") return null;
244
279
  const o = v as Record<string, unknown>;