create-venture 0.1.1 → 0.2.0

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/index.mjs +163 -22
  2. package/package.json +2 -2
package/index.mjs CHANGED
@@ -1,15 +1,26 @@
1
1
  #!/usr/bin/env node
2
- // create-venture: Naive's generator, the way create-next-app is Next.js's (I-1, I-3, I-7). It asks for a
3
- // name, offers the recommended defaults or lets you customize them, creates the venture in a new or
4
- // empty directory, installs the framework with your package manager, and initializes git.
2
+ // create-venture: Naive's generator, the way create-next-app is Next.js's (I-1, I-3, I-7, EE-082). Before
3
+ // anything is scaffolded it asks what you are building (described in a few words and classified by a
4
+ // model, or picked from the list) and what role you hold, then who else is founding it, the phase, and
5
+ // which functions (hats) each person owns. The venture type decides the packages and first decisions:
6
+ // a VC fund starts with a thesis and terms, a software startup with its product questions.
5
7
  //
6
- // npx create-venture [dir] [--yes] [--phase <phase>] [--layers a,b] [--legal-form <form>] [--dump <file>]
8
+ // npx create-venture [dir] [--yes] [--type <type> | --describe "<a few words>"] [--role <role>]
9
+ // [--name "<your name>"] [--team "<name>, <name>"] [--phase <phase>]
10
+ // [--source <function>=<system>[:<address>],... ...] [--rho-fixture]
11
+ // [--layers a,b] [--legal-form <form>] [--dump <file>]
7
12
  // [--use-npm | --use-pnpm | --use-yarn | --use-bun] [--skip-install] [--no-git]
8
13
  // [--framework <spec>] [--reset-preferences]
9
14
  //
10
- // --yes takes the recommended defaults: the discovery phase, the phase's packages, checks on, and
11
- // AGENTS.md. Answers to the setup questions are kept
12
- // in ~/.config/naive/create-venture.json, so the next venture can reuse them.
15
+ // --source says where a function keeps its truth (schema/systems-catalog.json), recorded as pointers in
16
+ // knowledge/sources.json: --source products=local-repo:../app,github:acme/app,vercel:acme/app
17
+ // --source finance=rho. Rho is synced at setup: its sandbox, the founder's own with RHO_API_TOKEN, or a
18
+ // saved answer with --rho-fixture (offline; recorded as a fixture).
19
+ //
20
+ // --yes takes the recommended defaults: a software startup, you as its solo founder wearing every hat,
21
+ // the discovery phase, the type's packages, checks on, and AGENTS.md. --describe classifies only when
22
+ // TYPESAFE_API_KEY is set; otherwise pick with --type. Answers are kept in
23
+ // ~/.config/naive/create-venture.json, so the next venture can reuse them.
13
24
 
14
25
  import { spawnSync } from "node:child_process";
15
26
  import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
@@ -30,11 +41,13 @@ const fw = JSON.parse(readFileSync(join(framework, "package.json"), "utf8"));
30
41
  const { layers: PHASE_LAYERS } = JSON.parse(readFileSync(join(framework, "schema/scaffold/defaults.json"), "utf8"));
31
42
  const PHASES = Object.keys(PHASE_LAYERS);
32
43
  const LAYERS = ["purpose", "knowledge", "operations", "evidence", "governance", "interface"];
44
+ // The legal forms venture.json accepts, from its schema, so the question can only offer valid answers.
45
+ const FORMS = JSON.parse(readFileSync(join(framework, "schema/venture.schema.json"), "utf8")).properties.form.properties.legal_form.enum;
33
46
 
34
47
  const argv = process.argv.slice(2);
35
48
  const opt = (n) => { const i = argv.indexOf(`--${n}`); return i >= 0 ? argv[i + 1] : undefined; };
36
49
  const flag = (n) => argv.includes(`--${n}`);
37
- const valued = new Set(["phase", "layers", "legal-form", "dump", "framework"]);
50
+ const valued = new Set(["phase", "layers", "legal-form", "dump", "framework", "type", "describe", "role", "name", "team"]);
38
51
  const positional = argv.filter((a, i) => !a.startsWith("--") && !(i > 0 && valued.has(argv[i - 1].slice(2))));
39
52
  const bold = (s) => (process.stdout.isTTY ? `\x1b[1m${s}\x1b[22m` : s);
40
53
  const cyan = (s) => (process.stdout.isTTY ? `\x1b[36m${s}\x1b[39m` : s);
@@ -73,20 +86,111 @@ const id = slug(basename(dir));
73
86
  if (!id) fail(`${basename(dir)} has no letters or digits to make a venture id from`);
74
87
  if (existsSync(dir) && readdirSync(dir).length) fail(`${dir} already exists and is not empty. create-venture only creates new ventures; to bring an existing project in, create one here and run naive port or naive migrate.`);
75
88
 
76
- // 2. Defaults, the last venture's answers, or your own (I-3, I-7).
89
+ // The framework's registries: venture types, roles and functions (EE-082).
90
+ const { types: TYPES, roles: ROLES, classify, canClassify } = await import(join(framework, "schema/venture-type.mjs")).then((m) => ({ types: m.types(), roles: m.roles(), classify: m.classify, canClassify: m.canClassify }));
91
+ const DEPARTMENTS = JSON.parse(readFileSync(join(framework, "operations/departments.json"), "utf8")).departments;
92
+ const deptLabel = (d) => DEPARTMENTS.find((x) => x.id === d)?.label ?? d;
93
+ const roleOf = (r) => ROLES.find((x) => x.id === r);
94
+ const gitName = () => spawnSync("git", ["config", "user.name"], { encoding: "utf8" }).stdout?.trim() || null;
95
+
96
+ // 2. What you are building: in a few words for the model, or from the list. Nothing is scaffolded until it is set.
97
+ let vtype = opt("type") ? TYPES.find((t) => t.id === opt("type")) : null;
98
+ if (opt("type") && !vtype) fail(`unknown venture type ${opt("type")}: one of ${TYPES.map((t) => t.id).join(", ")}`);
99
+ const pickFromList = async () => {
100
+ TYPES.forEach((t, i) => console.log(` ${String(i + 1).padStart(2)}. ${t.label}: ${t.description}`));
101
+ const a = await ask("Which kind of venture is it? (number or id)", "1");
102
+ const t = TYPES[Number(a) - 1] ?? TYPES.find((x) => x.id === a);
103
+ if (!t) { console.log(" choose a number from the list"); if (piped && !piped.length) fail("no answer left to choose with"); return pickFromList(); }
104
+ return t;
105
+ };
106
+ const classified = async (words) => {
107
+ if (!canClassify()) { console.log(" (no TYPESAFE_API_KEY, so pick from the list)"); return null; }
108
+ try {
109
+ const c = classify(words);
110
+ if (c.uncertain) { console.log(` not sure what kind of venture that is (best guess ${c.ranked[0].label}, ${Math.round(c.ranked[0].p * 100)}%), so pick from the list`); return null; }
111
+ return TYPES.find((t) => t.id === c.best.id);
112
+ } catch (e) { console.log(` ${e.message}; pick from the list`); return null; }
113
+ };
114
+ if (!vtype && opt("describe")) {
115
+ vtype = await classified(opt("describe"));
116
+ if (!vtype && !interactive) fail(`could not classify "${opt("describe")}": pass --type, one of ${TYPES.map((t) => t.id).join(", ")}`);
117
+ if (vtype) console.log(` ${opt("describe")}: a ${vtype.label.toLowerCase()}`);
118
+ }
119
+ if (!vtype && interactive) {
120
+ const words = await ask("What are you building? Describe it in a few words, or press Enter to pick from the list", "");
121
+ if (words) {
122
+ const t = await classified(words);
123
+ if (t && (await choose(`That sounds like a ${t.label.toLowerCase()}. Is that right?`, ["yes", "no"], "yes")) === "yes") vtype = t;
124
+ }
125
+ if (!vtype) vtype = await pickFromList();
126
+ }
127
+ // --yes with no type: the recommended default, a software startup.
128
+ if (!vtype) vtype = TYPES.find((t) => t.id === "software-startup");
129
+
130
+ // 3. Your role in it, from the roles this type has. Investors are a role too: a general partner runs a fund.
131
+ let role = opt("role") ?? (interactive ? await choose("What is your role in it?", vtype.roles, vtype.default_role) : vtype.default_role);
132
+ if (!vtype.roles.includes(role)) fail(`a ${vtype.label.toLowerCase()} has no ${role} role: one of ${vtype.roles.join(", ")}`);
133
+
134
+ // 4. You, and whether you are building it solo or with a team.
135
+ const me = opt("name") ?? (interactive ? await ask("What is your name?", gitName() ?? "Founder") : gitName() ?? "Founder");
136
+ let others = opt("team") ? opt("team").split(",").map((x) => x.trim()).filter(Boolean) : [];
137
+ if (!opt("team") && interactive && (await choose("Are you building it solo or with a team?", ["solo", "team"], prefs?.mode ?? "solo")) === "team") {
138
+ others = (await ask("Who else is founding it? (names, comma separated)", "")).split(",").map((x) => x.trim()).filter(Boolean);
139
+ if (!others.length) console.log(" no other names, so you are building it solo");
140
+ }
141
+ const mode = others.length ? "team" : "solo";
142
+ const coRole = vtype.id === "vc-fund" ? "general-partner" : "co-founder";
143
+ const people = [{ id: slug(me) || "founder", name: me, role }, ...others.map((n) => ({ id: slug(n), name: n, role: coRole }))];
144
+ if (new Set(people.map((p) => p.id)).size !== people.length) fail("two people would have the same id; give each a distinct name");
145
+
146
+ // 5. The phase, the hats, and the rest of the defaults (I-3, I-7).
77
147
  const recommended = { phase: "discovery", layers: null, legalForm: null, dump: null };
78
148
  let answers = { ...recommended };
79
149
  const given = opt("phase") || opt("layers") || opt("legal-form") || opt("dump");
80
150
  if (given) answers = { phase: opt("phase") ?? "discovery", layers: opt("layers") ?? null, legalForm: opt("legal-form") ?? null, dump: opt("dump") ?? null };
81
- else if (interactive) {
151
+ else if (interactive) answers.phase = await choose("Which phase is the venture in?", PHASES, prefs?.phase ?? "discovery");
152
+ // A solo founder owns every function the type imports; on a team, each function is asked once.
153
+ const owners = people.filter((p) => roleOf(p.role)?.owns_when_solo);
154
+ for (const p of people) p.hats = [];
155
+ if (owners.length === 1 || !interactive) for (const d of vtype.departments) (owners[0] ?? { hats: [] }).hats.push(d);
156
+ else for (const d of vtype.departments) {
157
+ const who = await choose(`Who owns ${deptLabel(d)}?`, owners.map((p) => p.id), owners[0].id);
158
+ owners.find((p) => p.id === who).hats.push(d);
159
+ }
160
+ // 6. Where each function keeps its truth (EE-083): pointers to its systems of record, in the founder's words when none fits.
161
+ const CATALOG = JSON.parse(readFileSync(join(framework, "schema/systems-catalog.json"), "utf8"));
162
+ const asked = [...vtype.departments.filter((d) => CATALOG.functions[d]), "legal"];
163
+ const parseSystems = (text) => {
164
+ const items = text.split(",").map((x) => x.trim()).filter(Boolean);
165
+ const systems = items.map((x) => { const i = x.indexOf(":"); const sys = i < 0 ? x : x.slice(0, i); return CATALOG.systems[sys] ? { system: sys, ...(i < 0 ? {} : { address: x.slice(i + 1) }) } : null; });
166
+ return systems.every(Boolean) ? { systems } : { systems: [], note: text.trim() };
167
+ };
168
+ const functions = {};
169
+ const sourceFlags = argv.flatMap((a, i) => (a === "--source" ? [argv[i + 1]] : []));
170
+ for (const f of sourceFlags) {
171
+ const [fn, ...rest] = f.split("=");
172
+ if (!CATALOG.functions[fn]) fail(`--source ${f}: ${fn} is not a function systems are kept for (${Object.keys(CATALOG.functions).join(", ")})`);
173
+ functions[fn] = parseSystems(rest.join("="));
174
+ for (const s of functions[fn].systems) if (!CATALOG.functions[fn].includes(s.system)) fail(`--source ${f}: ${s.system} is not offered for ${fn} (${CATALOG.functions[fn].join(", ")})`);
175
+ }
176
+ if (interactive && !sourceFlags.length) {
177
+ console.log("\nNow point Naive at where each function keeps its truth. Name systems (comma separated, add :address if you know it), type none, or say it in your own words.");
178
+ for (const fn of asked) {
179
+ const options = CATALOG.functions[fn];
180
+ const a = await ask(`Where is the source of truth for ${fn === "legal" ? "legal documents" : deptLabel(fn)}? ${options.join(" / ")} / none`, "none");
181
+ if (a.toLowerCase() === "none") continue;
182
+ functions[fn] = parseSystems(a);
183
+ }
184
+ }
185
+
186
+ if (interactive && !given) {
82
187
  const modes = prefs ? ["recommended", "previous", "customize"] : ["recommended", "customize"];
83
- const mode = await choose("Would you like to use the recommended Naive defaults?", modes, "recommended");
84
- if (mode === "previous") answers = { ...recommended, ...prefs };
85
- if (mode === "customize") {
86
- answers.phase = await choose("Which phase is the venture in?", PHASES, "discovery");
87
- const l = await ask(`Which layers should be on? (comma separated, from ${LAYERS.join(", ")}; empty for the phase's defaults: ${PHASE_LAYERS[answers.phase].join(", ")})`, "");
188
+ const pick = await choose("Would you like to use the recommended Naive defaults for the rest?", modes, "recommended");
189
+ if (pick === "previous") answers = { ...answers, layers: prefs.layers ?? null, legalForm: prefs.legalForm ?? null };
190
+ if (pick === "customize") {
191
+ const l = await ask(`Which layers should be on? (comma separated, from ${LAYERS.join(", ")}; empty for the defaults)`, "");
88
192
  answers.layers = l || null;
89
- const f = await choose("Is the venture a legal entity yet?", ["none", "llc", "corporation", "partnership", "nonprofit"], "none");
193
+ const f = await choose("Is the venture a legal entity yet?", FORMS, "none");
90
194
  answers.legalForm = f === "none" ? null : f;
91
195
  const d = await ask("Paste the path of an idea dump to start from (optional)", "");
92
196
  answers.dump = d || null;
@@ -94,6 +198,7 @@ else if (interactive) {
94
198
  }
95
199
  if (!PHASES.includes(answers.phase)) fail(`unknown phase ${answers.phase}: one of ${PHASES.join(", ")}`);
96
200
  if (answers.dump && !existsSync(resolve(answers.dump))) fail(`no idea dump at ${answers.dump}`);
201
+ if (answers.legalForm && !FORMS.includes(answers.legalForm)) fail(`unknown legal form ${answers.legalForm}: one of ${FORMS.join(", ")}`);
97
202
 
98
203
  // 3. The package manager: the flag, else whichever ran this (npm_config_user_agent), else npm.
99
204
  const pm = ["npm", "pnpm", "yarn", "bun"].find((p) => flag(`use-${p}`)) ?? (process.env.npm_config_user_agent ?? "").split("/")[0].replace(/^npx$/, "npm") ?? "npm";
@@ -101,20 +206,55 @@ const manager = ["npm", "pnpm", "yarn", "bun"].includes(pm) ? pm : "npm";
101
206
  rl?.close();
102
207
 
103
208
  // 4. The venture itself, by the framework's own scaffold.
104
- console.log(`\nCreating a new Naive venture in ${bold(dir)}.\n`);
105
- const init = spawnSync("node", [join(framework, "schema/init-venture.mjs"), dir, id, "--phase", answers.phase,
209
+ console.log(`\nCreating a new ${vtype.label.toLowerCase()} in ${bold(dir)}, with ${mode === "solo" ? `${me} as its ${role}, wearing every hat` : `${people.length} founders`}.\n`);
210
+ const init = spawnSync("node", [join(framework, "schema/init-venture.mjs"), dir, id, "--phase", answers.phase, "--type", vtype.id,
106
211
  ...(answers.layers ? ["--layers", answers.layers] : []), ...(answers.legalForm ? ["--legal-form", answers.legalForm] : [])], { encoding: "utf8" });
107
212
  if (init.status !== 0) fail((init.stderr || init.stdout).trim());
108
213
  const vfile = join(dir, "venture.json");
109
214
  const venture = JSON.parse(readFileSync(vfile, "utf8"));
110
215
  venture.name = basename(dir);
216
+ if (mode === "team") venture.members = people.slice(1).map((p) => ({ role: p.role === "general-partner" ? "partner" : p.role, kind: "person" }));
111
217
  writeFileSync(vfile, JSON.stringify(venture, null, 2) + "\n");
112
218
 
219
+ // people/: the roster and one file per founder, the venture.json of human capital (EE-082).
113
220
  const { agentFromEnv, record } = await import(join(framework, "interface/provenance.mjs"));
221
+ const today = new Date().toISOString().slice(0, 10);
222
+ mkdirSync(join(dir, "people/founders"), { recursive: true });
223
+ const asPerson = agentFromEnv("person", "cli", { id: people[0].id });
224
+ for (const p of people.filter((x) => roleOf(x.role)?.founder)) {
225
+ p.founder_file = `people/founders/${p.id}.json`;
226
+ writeFileSync(join(dir, p.founder_file), JSON.stringify({ id: p.id, name: p.name, role: p.role, hats: p.hats, joined: today }, null, 2) + "\n");
227
+ record(dir, { entity: p.founder_file, route: `people/founders/${p.id}`, activity: "write", agent: asPerson, note: `${p.name}, ${p.role}, as answered at create-venture` });
228
+ }
229
+ writeFileSync(join(dir, "people/people.json"), JSON.stringify({ venture: id, mode, people: people.map(({ id: pid, name, role: r, hats, founder_file }) => ({ id: pid, name, role: r, hats, ...(founder_file ? { founder_file } : {}) })), updated: today }, null, 2) + "\n");
230
+ record(dir, { entity: "people/people.json", route: "people", activity: "write", agent: asPerson, note: `${mode}: ${people.map((p) => `${p.name} (${p.role})`).join(", ")}` });
114
231
  if (answers.dump) {
115
232
  mkdirSync(join(dir, "raw"), { recursive: true });
116
233
  copyFileSync(resolve(answers.dump), join(dir, "raw/dump.md"));
117
- record(dir, { entity: "raw/dump.md", route: "raw/dump", activity: "create", agent: agentFromEnv("person", "create-venture"), note: `the founder's idea dump, verbatim, from ${resolve(answers.dump)}` });
234
+ record(dir, { entity: "raw/dump.md", route: "raw/dump", activity: "write", agent: agentFromEnv("person", "cli"), note: `the founder's idea dump, verbatim, from ${resolve(answers.dump)}` });
235
+ }
236
+
237
+ // knowledge/sources.json: each function's systems of record, as pointers (EE-083). Rho is read by code, so it is synced now.
238
+ const rhoScript = join(framework, "platform/integrations/rho.mjs");
239
+ let rhoNote = null;
240
+ for (const f of Object.values(functions)) for (const s of f.systems) {
241
+ s.status = "pointed";
242
+ if (s.system !== "rho") continue;
243
+ if (!existsSync(rhoScript)) { rhoNote = "Rho: recorded as pointed; this copy of Naive has no Rho integration"; continue; }
244
+ const fixture = flag("rho-fixture") ? join(framework, "platform/integrations/fixtures/rho-sandbox.json") : null;
245
+ const live = !fixture && process.env.RHO_API_TOKEN;
246
+ const r = spawnSync("node", [rhoScript, "sync", "--venture", dir, ...(fixture ? ["--fixture", fixture] : live ? ["--live"] : [])], { encoding: "utf8", env: process.env });
247
+ if (r.status === 0) {
248
+ Object.assign(s, { status: "connected", synced: new Date().toISOString().replace(/\.\d+Z$/, "Z"), ...(fixture ? { fixture: true } : {}), address: s.address ?? (fixture ? "saved sandbox answer" : live ? "live" : "sandbox") });
249
+ rhoNote = `Rho: synced ${fixture ? "from a saved sandbox answer (a fixture, not a live account)" : live ? "from your Rho account" : "from Rho's sandbox (a fictional company)"} into money/`;
250
+ } else rhoNote = `Rho: could not sync (${(r.stderr || r.stdout).trim().split("\n").at(-1)}); recorded as pointed`;
251
+ }
252
+ if (Object.keys(functions).length) {
253
+ const map = join(dir, "knowledge/sources.json");
254
+ mkdirSync(dirname(map), { recursive: true });
255
+ writeFileSync(map, JSON.stringify({ sources: [], records: {}, truth: "local", functions }, null, 2) + "\n");
256
+ record(dir, { entity: "knowledge/sources.json", route: "knowledge/knowledge-sources", activity: "write", agent: asPerson,
257
+ note: `systems of record: ${Object.entries(functions).map(([fn, f]) => `${fn} ${f.systems.map((s) => `${s.system} (${s.status})`).join(", ") || "in the founder's words"}`).join("; ")}` });
118
258
  }
119
259
 
120
260
  // The venture's package.json: the framework as a dev dependency and the four scripts (I-17).
@@ -126,7 +266,7 @@ writeFileSync(join(dir, "package.json"), JSON.stringify({
126
266
  scripts: { dev: "naive dev", build: "naive build", start: "naive start", check: "naive check" },
127
267
  devDependencies: { [fw.name]: spec },
128
268
  }, null, 2) + "\n");
129
- writeFileSync(join(dir, ".gitignore"), "node_modules/\n.naive/\n.DS_Store\n# Founder-supplied originals stay out of git (EE-070).\n**/raw/**/original.*\n");
269
+ writeFileSync(join(dir, ".gitignore"), "node_modules/\n.naive/\n.DS_Store\n# Founder-supplied originals stay out of git (EE-070).\n**/raw/**/original.*\n# A data stream's activity (bank transactions, CRM events) stays out of git; only its stream.json and summary.md are versioned.\n/*/_streams/*/*\n!/*/_streams/*/stream.json\n!/*/_streams/*/summary.md\n");
130
270
 
131
271
  // Editor completion for records (I-26): every record file is mapped to its JSON Schema in the installed framework.
132
272
  const { routes } = await import(join(framework, "interface/router.mjs"));
@@ -151,13 +291,14 @@ if (!flag("no-git") && spawnSync("git", ["--version"]).status === 0 && spawnSync
151
291
  git = c.status === 0 ? "initialized, with a first commit" : "initialized; the first commit was skipped because git has no user.name and user.email";
152
292
  } else if (!flag("no-git")) git = "skipped (already inside a git repository)";
153
293
 
154
- try { mkdirSync(dirname(prefsFile), { recursive: true }); writeFileSync(prefsFile, JSON.stringify({ phase: answers.phase, layers: answers.layers, legalForm: answers.legalForm }, null, 2) + "\n"); } catch {}
294
+ try { mkdirSync(dirname(prefsFile), { recursive: true }); writeFileSync(prefsFile, JSON.stringify({ type: vtype.id, role, mode, phase: answers.phase, layers: answers.layers, legalForm: answers.legalForm }, null, 2) + "\n"); } catch {}
155
295
 
156
296
  const runScript = (s) => (manager === "npm" ? `npm run ${s}` : `${manager} ${s}`);
157
297
  const cd = relative(process.cwd(), dir) || ".";
298
+ const legal = functions.legal?.systems.length ? null : "Legal documents: none connected yet. Point Naive at them later with --source legal=google-drive:<folder> (or docusign, local-folder), and draft the founder agreement with the founding-agreement kit.";
158
299
  console.log(`
159
300
  ${bold("Success!")} Created ${basename(dir)} at ${dir}
160
- Git: ${git}
301
+ Git: ${git}${rhoNote ? `\n${rhoNote}` : ""}${legal ? `\n${legal}` : ""}
161
302
 
162
303
  Inside that directory, you can run:
163
304
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-venture",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Create a new Naive venture: npx create-venture my-venture",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,6 +20,6 @@
20
20
  "LICENSE"
21
21
  ],
22
22
  "dependencies": {
23
- "naive-framework": "^0.1.0"
23
+ "naive-framework": "^0.2.0"
24
24
  }
25
25
  }