src / db.ts
import { readFile, writeFile, mkdir } from "fs/promises";
import { join } from "path";
import { homedir } from "os";
// ---------------------------------------------------------------------------
// Data model
// ---------------------------------------------------------------------------
export type StartupStage =
| "idea" // just a hypothesis
| "problem" // validated a real problem exists
| "solution" // built something testable
| "traction" // have early paying users
| "growth" // scaling
| "paused"
| "killed";
export interface Startup {
id: string;
name: string;
stage: StartupStage;
oneLiner: string; // "X for Y who Z"
hypothesis: string; // core belief to validate
icp: string; // ideal customer profile
problemStatement: string; // the pain being solved
revenueModel: string; // how it makes money
pricingHypothesis: string; // what people will pay and why
createdAt: string;
updatedAt: string;
}
export interface CustomerProblem {
id: string;
startupId: string;
source: string; // "interview", "survey", "observation", "support ticket", etc.
description: string; // what the problem is
verbatim: string; // exact quote from the customer
frequency: "rare" | "occasional" | "frequent" | "constant";
severity: "minor" | "moderate" | "critical";
workaround: string; // what they do today instead
createdAt: string;
}
export interface Experiment {
id: string;
startupId: string;
assumption: string; // "We believe that..."
method: string; // how the experiment was run
successCriteria: string; // measurable definition of pass
result: "pending" | "validated" | "invalidated" | "inconclusive";
learnings: string; // what was discovered
evidence: string; // data/quotes that support the result
testedAt: string | null;
createdAt: string;
}
export interface Decision {
id: string;
startupId: string;
decision: string; // what was decided
options: string; // alternatives considered
rationale: string; // why this choice
date: string;
outcome: string; // what happened as a result (filled in later)
createdAt: string;
}
export interface Competitor {
id: string;
startupId: string;
name: string;
url: string;
positioning: string; // how they describe themselves
targetCustomer: string;
pricing: string;
strengths: string;
weaknesses: string; // gaps you can exploit
differentiator: string; // how you're different
lastScanned: string;
createdAt: string;
}
export interface FounderDB {
startups: Startup[];
customerProblems: CustomerProblem[];
experiments: Experiment[];
decisions: Decision[];
competitors: Competitor[];
}
// ---------------------------------------------------------------------------
// Persistence
// ---------------------------------------------------------------------------
function makeId(): string {
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
}
function todayStr(): string {
return new Date().toISOString().slice(0, 10);
}
function dbPath(dataDir: string): string {
return join(dataDir, "founder.json");
}
async function loadDB(dataDir: string): Promise<FounderDB> {
await mkdir(dataDir, { recursive: true });
let raw: string;
try {
raw = await readFile(dbPath(dataDir), "utf8");
} catch (err: unknown) {
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
return { startups: [], customerProblems: [], experiments: [], decisions: [], competitors: [] };
}
throw err;
}
const db = JSON.parse(raw) as Partial<FounderDB>;
db.startups ??= [];
db.customerProblems ??= [];
db.experiments ??= [];
db.decisions ??= [];
db.competitors ??= [];
return db as FounderDB;
}
async function saveDB(dataDir: string, db: FounderDB): Promise<void> {
await mkdir(dataDir, { recursive: true });
await writeFile(dbPath(dataDir), JSON.stringify(db, null, 2), "utf8");
}
// ---------------------------------------------------------------------------
// Data access helpers — exported for toolsProvider
// ---------------------------------------------------------------------------
export { makeId, todayStr, loadDB, saveDB };
export function getDataDir(configPath: string): string {
const p = configPath.trim() || join(homedir(), "founder-data");
return p.startsWith("~/") ? join(homedir(), p.slice(2)) : p;
}