← back to Kickbacks Ai Upstream

src/config.ts

118 lines

// User-editable extension config at ~/.vibe-ads/config.json.
//
// Shape (all fields optional):
//   {
//     "backendBaseUrl":      "https://kickbacks-backend-gmdaqm2c7q-uw.a.run.app",
//     "localVsixPath":       "C:/path/to/kickbacks.vsix", // mtime-watched
//     "updatePollIntervalMs": 90000,                   // remote poll cadence
//     "debugMode":           false                     // enable dlog writes (debug logging)
//   }
//
// Reads are best-effort: a missing/malformed file resolves to the defaults so
// activation can never be broken by config. Writes go through ensureFile()
// which materialises the file with the current effective defaults the first
// time the user opens the editor.

import { homedir } from "node:os";
import { join } from "node:path";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";

export interface VibeAdsConfig {
  backendBaseUrl: string;
  /** Base URL for the self-update manifest poll. When set, the UpdateClient
   *  uses this instead of backendBaseUrl — so self-update can point at the
   *  public site while auth/metrics stay on the local backend. Empty string
   *  falls through to the env var, then to the compiled-in default. */
  updateBaseUrl: string;
  localVsixPath: string;
  updatePollIntervalMs: number;
  /** When true, the extension behaves as if `VIBE_ADS_DEBUG=1` or the
   *  `~/.vibe-ads/debug.enabled` sentinel were present: dlog writes events.
   *  False (or unset) = off. */
  debugMode: boolean;
}

export const DEFAULT_POLL_MS = 90_000;

export function configDir(): string {
  return join(homedir(), ".vibe-ads");
}
export function configPath(): string {
  return join(configDir(), "config.json");
}

function defaults(): VibeAdsConfig {
  return { backendBaseUrl: "", updateBaseUrl: "", localVsixPath: "",
           updatePollIntervalMs: DEFAULT_POLL_MS, debugMode: false };
}

export function readConfig(): VibeAdsConfig {
  const out = defaults();
  try {
    const raw = readFileSync(configPath(), "utf8");
    const j = JSON.parse(raw) as Partial<VibeAdsConfig>;
    if (typeof j.backendBaseUrl === "string") out.backendBaseUrl = j.backendBaseUrl.trim();
    if (typeof j.updateBaseUrl === "string") out.updateBaseUrl = j.updateBaseUrl.trim();
    if (typeof j.localVsixPath === "string") out.localVsixPath = j.localVsixPath.trim();
    if (typeof j.updatePollIntervalMs === "number" && j.updatePollIntervalMs >= 10_000) {
      out.updatePollIntervalMs = j.updatePollIntervalMs;
    }
    if (typeof j.debugMode === "boolean") out.debugMode = j.debugMode;
  } catch { /* absent or malformed -> defaults */ }
  return out;
}

/** Materialise the config file with current defaults if it doesn't exist.
 *  Used by the debug "Edit config" entry so the user always opens a real,
 *  documented file. Returns the absolute path either way. */
export function ensureConfigFile(): string {
  const p = configPath();
  if (!existsSync(p)) {
    try { mkdirSync(configDir(), { recursive: true }); } catch { /* ok */ }
    const tmpl = {
      // Backend / manifest base URL. Empty string -> use VIBE_ADS_BASE env
      // var, else fall back to production Cloud Run. Used by auth, metrics,
      // killswitch, earnings, consent.
      backendBaseUrl: "",
      // Self-update manifest base URL. Empty -> KICKBACKS_UPDATE_BASE env var
      // -> public site default. Separates the update path (can be public)
      // from the API path (may still be localhost during migration).
      updateBaseUrl: "",
      // Optional local-source update: an absolute path to a .vsix file. When
      // set, the extension watches its mtime and installs whenever it changes
      // — useful for dev rigs without a manifest server. Empty -> disabled.
      localVsixPath: "",
      // Remote-manifest poll cadence in ms. Clamped to >= 10s.
      updatePollIntervalMs: DEFAULT_POLL_MS,
      // Debug mode. Equivalent to setting `VIBE_ADS_DEBUG=1` or touching
      // ~/.vibe-ads/debug.enabled: enables dlog writes. Off in prod.
      debugMode: false,
    };
    try { writeFileSync(p, JSON.stringify(tmpl, null, 2) + "\n", "utf8"); }
    catch { /* best-effort */ }
  }
  return p;
}

const DEFAULT_BACKEND_BASE = "https://kickbacks-backend-gmdaqm2c7q-uw.a.run.app";

/** Resolve the effective backend base URL: config file > env > default.
 *  Non-loopback HTTP is refused at the call site in extension.ts (this fn
 *  is pure and side-effect-free so it can be unit-tested without mocking). */
export function resolveBackendBase(cfg: VibeAdsConfig, env: string | undefined): string {
  if (cfg.backendBaseUrl) return cfg.backendBaseUrl;
  if (env) return env;
  return DEFAULT_BACKEND_BASE;
}

const DEFAULT_UPDATE_BASE = "https://kickbacks-public-gmdaqm2c7q-uw.a.run.app";

/** Resolve the self-update manifest base URL: config > env > public site.
 *  Separated from the API base so self-update works over the public internet
 *  while auth/metrics can still hit the local backend during migration. */
export function resolveUpdateBase(cfg: VibeAdsConfig, env: string | undefined): string {
  if (cfg.updateBaseUrl) return cfg.updateBaseUrl;
  if (env) return env;
  return DEFAULT_UPDATE_BASE;
}