()
| 271 | } |
| 272 | |
| 273 | function freshDetectEngineTier(): EngineDetect { |
| 274 | const now = Date.now(); |
| 275 | let parsed: Record<string, unknown> | null = null; |
| 276 | |
| 277 | // execFileSync (not execSync) avoids shell redirection — portable to |
| 278 | // environments where `2>/dev/null` is bash-specific. The stdio array |
| 279 | // suppresses stderr without invoking a shell. |
| 280 | try { |
| 281 | const out = execFileSync("gbrain", ["doctor", "--json", "--fast"], { |
| 282 | encoding: "utf-8", |
| 283 | timeout: 5000, |
| 284 | stdio: ["ignore", "pipe", "ignore"], |
| 285 | }); |
| 286 | parsed = JSON.parse(out); |
| 287 | } catch (err: unknown) { |
| 288 | // execFileSync throws on non-zero exit; stdout is still on the error |
| 289 | // object. gbrain doctor exits 1 whenever health_score < 100, which is |
| 290 | // essentially always on fresh installs (resolver_health warnings are |
| 291 | // normal). Recover stdout and re-parse. See #1415. |
| 292 | try { |
| 293 | const stdout = (err as { stdout?: Buffer | string })?.stdout ?? ""; |
| 294 | const stdoutStr = typeof stdout === "string" ? stdout : stdout.toString("utf-8"); |
| 295 | if (stdoutStr) parsed = JSON.parse(stdoutStr); |
| 296 | } catch (parseErr) { |
| 297 | logGbrainError("doctor_parse_failure", String(parseErr)); |
| 298 | } |
| 299 | } |
| 300 | |
| 301 | let engine: EngineTier = |
| 302 | parsed?.engine === "supabase" ? "supabase" : |
| 303 | parsed?.engine === "pglite" ? "pglite" : "unknown"; |
| 304 | |
| 305 | // gbrain >=0.25 ships schema_version:2 doctor output which dropped the |
| 306 | // top-level `engine` field. Fall back to gbrain's config.json (respects |
| 307 | // GBRAIN_HOME). "supabase" here means "remote postgres" — gbrain config |
| 308 | // uses engine:"postgres" for real Supabase AND any other remote postgres |
| 309 | // (e.g. local-postgres-for-testing). Downstream sync code treats them the |
| 310 | // same, so the label compression is intentional. |
| 311 | if (engine === "unknown") { |
| 312 | try { |
| 313 | const cfg = JSON.parse(readFileSync(gbrainConfigPath(), "utf-8")); |
| 314 | if (cfg?.engine === "pglite") engine = "pglite"; |
| 315 | else if (cfg?.engine === "postgres" || cfg?.database_url) engine = "supabase"; |
| 316 | } catch (cfgErr) { |
| 317 | logGbrainError("config_read_failure", String(cfgErr)); |
| 318 | } |
| 319 | } |
| 320 | |
| 321 | return { |
| 322 | engine, |
| 323 | supabase_url: parsed?.supabase_url as string | undefined, |
| 324 | detected_at: now, |
| 325 | schema_version: 1, |
| 326 | }; |
| 327 | } |
| 328 | |
| 329 | // ── Public: parseSkillManifest ──────────────────────────────────────────── |
| 330 |
no test coverage detected