create-venture 0.2.0 → 0.3.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 +58 -20
  2. package/package.json +2 -2
package/index.mjs CHANGED
@@ -10,7 +10,7 @@
10
10
  // [--source <function>=<system>[:<address>],... ...] [--rho-fixture]
11
11
  // [--layers a,b] [--legal-form <form>] [--dump <file>]
12
12
  // [--use-npm | --use-pnpm | --use-yarn | --use-bun] [--skip-install] [--no-git]
13
- // [--framework <spec>] [--reset-preferences]
13
+ // [--framework <spec>] [--reset-preferences] [--venture-name "<name>"] [--no-workspace]
14
14
  //
15
15
  // --source says where a function keeps its truth (schema/systems-catalog.json), recorded as pointers in
16
16
  // knowledge/sources.json: --source products=local-repo:../app,github:acme/app,vercel:acme/app
@@ -47,7 +47,7 @@ const FORMS = JSON.parse(readFileSync(join(framework, "schema/venture.schema.jso
47
47
  const argv = process.argv.slice(2);
48
48
  const opt = (n) => { const i = argv.indexOf(`--${n}`); return i >= 0 ? argv[i + 1] : undefined; };
49
49
  const flag = (n) => argv.includes(`--${n}`);
50
- const valued = new Set(["phase", "layers", "legal-form", "dump", "framework", "type", "describe", "role", "name", "team"]);
50
+ const valued = new Set(["phase", "layers", "legal-form", "dump", "framework", "type", "describe", "role", "name", "team", "layout", "venture-name"]);
51
51
  const positional = argv.filter((a, i) => !a.startsWith("--") && !(i > 0 && valued.has(argv[i - 1].slice(2))));
52
52
  const bold = (s) => (process.stdout.isTTY ? `\x1b[1m${s}\x1b[22m` : s);
53
53
  const cyan = (s) => (process.stdout.isTTY ? `\x1b[36m${s}\x1b[39m` : s);
@@ -75,13 +75,18 @@ const choose = async (q, options, fallback) => {
75
75
  return a;
76
76
  };
77
77
 
78
- // 1. The name and the directory.
78
+ // 1. The name and the directory. The defaults are "My first venture" in my-first-venture/, and either can
79
+ // be renamed later; a directory given on the command line names the venture unless --venture-name does.
80
+ const FIRST = "My first venture";
81
+ const WORKSPACE_FILE = "naive-workspace.json";
79
82
  let target = positional[0];
83
+ let ventureName = opt("venture-name");
80
84
  if (!target) {
81
- if (!interactive) fail("usage: create-venture <dir> [--yes]: name the directory for the new venture");
82
- target = await ask("What is your venture named?", "my-venture");
85
+ ventureName ??= interactive ? await ask("What is your venture named?", FIRST) : FIRST;
86
+ target = slug(ventureName);
83
87
  }
84
88
  const dir = resolve(target);
89
+ ventureName ??= basename(dir);
85
90
  const id = slug(basename(dir));
86
91
  if (!id) fail(`${basename(dir)} has no letters or digits to make a venture id from`);
87
92
  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.`);
@@ -149,13 +154,23 @@ let answers = { ...recommended };
149
154
  const given = opt("phase") || opt("layers") || opt("legal-form") || opt("dump");
150
155
  if (given) answers = { phase: opt("phase") ?? "discovery", layers: opt("layers") ?? null, legalForm: opt("legal-form") ?? null, dump: opt("dump") ?? null };
151
156
  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.
157
+ // The layout (EE-086): structured (knowledge/, meta/, the org chart in venture.json) unless --layout flat.
158
+ const layout = opt("layout") ?? "structured";
159
+ if (!["flat", "structured"].includes(layout)) fail(`unknown layout ${layout}: structured or flat`);
160
+ const structured = layout === "structured";
161
+ // A solo founder owns every function; on a team, each function is asked once. In the structured layout the
162
+ // functions are the org chart's (one per package, flat) and ownership goes in venture.json; in the flat one,
163
+ // the catalog's departments, worn as hats in people/.
164
+ const fnIds = structured ? vtype.packages : vtype.departments;
165
+ const fnLabel = (f) => (structured ? f : deptLabel(f));
153
166
  const owners = people.filter((p) => roleOf(p.role)?.owns_when_solo);
167
+ const ownerOf = {};
154
168
  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);
169
+ if (owners.length === 1 || !interactive) for (const f of fnIds) { ownerOf[f] = owners[0]?.id ?? null; owners[0]?.hats.push(f); }
170
+ else for (const f of fnIds) {
171
+ const who = await choose(`Who owns ${fnLabel(f)}?`, owners.map((p) => p.id), owners[0].id);
172
+ ownerOf[f] = who;
173
+ owners.find((p) => p.id === who).hats.push(f);
159
174
  }
160
175
  // 6. Where each function keeps its truth (EE-083): pointers to its systems of record, in the founder's words when none fits.
161
176
  const CATALOG = JSON.parse(readFileSync(join(framework, "schema/systems-catalog.json"), "utf8"));
@@ -207,30 +222,34 @@ rl?.close();
207
222
 
208
223
  // 4. The venture itself, by the framework's own scaffold.
209
224
  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,
225
+ const init = spawnSync("node", [join(framework, "schema/init-venture.mjs"), dir, id, "--phase", answers.phase, "--type", vtype.id, "--layout", layout,
211
226
  ...(answers.layers ? ["--layers", answers.layers] : []), ...(answers.legalForm ? ["--legal-form", answers.legalForm] : [])], { encoding: "utf8" });
212
227
  if (init.status !== 0) fail((init.stderr || init.stdout).trim());
213
228
  const vfile = join(dir, "venture.json");
214
229
  const venture = JSON.parse(readFileSync(vfile, "utf8"));
215
- venture.name = basename(dir);
230
+ venture.name = ventureName;
216
231
  if (mode === "team") venture.members = people.slice(1).map((p) => ({ role: p.role === "general-partner" ? "partner" : p.role, kind: "person" }));
232
+ // The org chart (EE-086): each function's owner. Hats are read from here, never stored twice.
233
+ if (structured) for (const f of Object.keys(venture.functions ?? {})) venture.functions[f].owner = ownerOf[f] ?? null;
217
234
  writeFileSync(vfile, JSON.stringify(venture, null, 2) + "\n");
235
+ // Where each logical file sits in this venture's layout (schema/venture-paths.mjs).
236
+ const { ventureFile: vf, physical } = await import(join(framework, "schema/venture-paths.mjs"));
218
237
 
219
238
  // people/: the roster and one file per founder, the venture.json of human capital (EE-082).
220
239
  const { agentFromEnv, record } = await import(join(framework, "interface/provenance.mjs"));
221
240
  const today = new Date().toISOString().slice(0, 10);
222
- mkdirSync(join(dir, "people/founders"), { recursive: true });
241
+ mkdirSync(vf(dir, "people/founders"), { recursive: true });
223
242
  const asPerson = agentFromEnv("person", "cli", { id: people[0].id });
224
243
  for (const p of people.filter((x) => roleOf(x.role)?.founder)) {
225
244
  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");
245
+ writeFileSync(vf(dir, p.founder_file), JSON.stringify({ id: p.id, name: p.name, role: p.role, ...(structured ? {} : { hats: p.hats }), joined: today }, null, 2) + "\n");
227
246
  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
247
  }
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");
248
+ writeFileSync(vf(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, ...(structured ? {} : { hats }), ...(founder_file ? { founder_file } : {}) })), updated: today }, null, 2) + "\n");
230
249
  record(dir, { entity: "people/people.json", route: "people", activity: "write", agent: asPerson, note: `${mode}: ${people.map((p) => `${p.name} (${p.role})`).join(", ")}` });
231
250
  if (answers.dump) {
232
- mkdirSync(join(dir, "raw"), { recursive: true });
233
- copyFileSync(resolve(answers.dump), join(dir, "raw/dump.md"));
251
+ mkdirSync(vf(dir, "raw"), { recursive: true });
252
+ copyFileSync(resolve(answers.dump), vf(dir, "raw/dump.md"));
234
253
  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
254
  }
236
255
 
@@ -250,7 +269,7 @@ for (const f of Object.values(functions)) for (const s of f.systems) {
250
269
  } else rhoNote = `Rho: could not sync (${(r.stderr || r.stdout).trim().split("\n").at(-1)}); recorded as pointed`;
251
270
  }
252
271
  if (Object.keys(functions).length) {
253
- const map = join(dir, "knowledge/sources.json");
272
+ const map = vf(dir, "knowledge/sources.json");
254
273
  mkdirSync(dirname(map), { recursive: true });
255
274
  writeFileSync(map, JSON.stringify({ sources: [], records: {}, truth: "local", functions }, null, 2) + "\n");
256
275
  record(dir, { entity: "knowledge/sources.json", route: "knowledge/knowledge-sources", activity: "write", agent: asPerson,
@@ -266,12 +285,12 @@ writeFileSync(join(dir, "package.json"), JSON.stringify({
266
285
  scripts: { dev: "naive dev", build: "naive build", start: "naive start", check: "naive check" },
267
286
  devDependencies: { [fw.name]: spec },
268
287
  }, null, 2) + "\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");
288
+ 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/knowledge/*/_streams/*/*\n!/knowledge/*/_streams/*/stream.json\n!/knowledge/*/_streams/*/summary.md\n");
270
289
 
271
290
  // Editor completion for records (I-26): every record file is mapped to its JSON Schema in the installed framework.
272
291
  const { routes } = await import(join(framework, "interface/router.mjs"));
273
292
  const schemas = new Map();
274
- for (const r of routes()) if (r.kind === "record" && r.schema && r.form !== "md") for (const f of r.files ?? []) if (f.endsWith(".json")) schemas.set(r.schema, [...(schemas.get(r.schema) ?? []), `/${f}`]);
293
+ for (const r of routes()) if (r.kind === "record" && r.schema && r.form !== "md") for (const f of r.files ?? []) if (f.endsWith(".json")) schemas.set(r.schema, [...(schemas.get(r.schema) ?? []), `/${physical(dir, f)}`]);
275
294
  mkdirSync(join(dir, ".vscode"), { recursive: true });
276
295
  writeFileSync(join(dir, ".vscode/settings.json"), JSON.stringify({
277
296
  "json.schemas": [...schemas].map(([schema, fileMatch]) => ({ fileMatch: [...new Set(fileMatch)], url: `./node_modules/${fw.name}/${schema}` })),
@@ -291,6 +310,24 @@ if (!flag("no-git") && spawnSync("git", ["--version"]).status === 0 && spawnSync
291
310
  git = c.status === 0 ? "initialized, with a first commit" : "initialized; the first commit was skipped because git has no user.name and user.email";
292
311
  } else if (!flag("no-git")) git = "skipped (already inside a git repository)";
293
312
 
313
+ // The workspace (the folder holding the founder's ventures, like a monorepo root): the nearest
314
+ // naive-workspace.json above the venture lists it; with none, the venture's parent folder becomes the
315
+ // workspace, named "Naive workspace" until the founder renames it, with an AGENTS.md for rules that hold
316
+ // across every venture. --no-workspace leaves the parent folder as it is.
317
+ let workspace = null;
318
+ if (!flag("no-workspace")) {
319
+ let up = dirname(dir);
320
+ while (!existsSync(join(up, WORKSPACE_FILE)) && dirname(up) !== up) up = dirname(up);
321
+ const root = existsSync(join(up, WORKSPACE_FILE)) ? up : dirname(dir);
322
+ const wf = join(root, WORKSPACE_FILE);
323
+ const ws = existsSync(wf) ? JSON.parse(readFileSync(wf, "utf8")) : { schema: "everything-ecosystem", schema_version: readFileSync(join(framework, "schema/VERSION"), "utf8").trim(), name: "Naive workspace", ventures: [] };
324
+ const rel = relative(root, dir);
325
+ if (!ws.ventures.includes(rel)) ws.ventures.push(rel);
326
+ writeFileSync(wf, JSON.stringify(ws, null, 2) + "\n");
327
+ if (!existsSync(join(root, "AGENTS.md"))) writeFileSync(join(root, "AGENTS.md"), `# ${ws.name}\n\nRules for agents across every venture in this workspace: who you are, how you work, what is off limits. A venture's own AGENTS.md adds to these, and the nearer file wins when two say the same thing.\n`);
328
+ workspace = { root, name: ws.name };
329
+ }
330
+
294
331
  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 {}
295
332
 
296
333
  const runScript = (s) => (manager === "npm" ? `npm run ${s}` : `${manager} ${s}`);
@@ -298,6 +335,7 @@ const cd = relative(process.cwd(), dir) || ".";
298
335
  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.";
299
336
  console.log(`
300
337
  ${bold("Success!")} Created ${basename(dir)} at ${dir}
338
+ Workspace: ${workspace ? `${workspace.name} at ${workspace.root} (${WORKSPACE_FILE})` : "none (--no-workspace)"}
301
339
  Git: ${git}${rhoNote ? `\n${rhoNote}` : ""}${legal ? `\n${legal}` : ""}
302
340
 
303
341
  Inside that directory, you can run:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-venture",
3
- "version": "0.2.0",
3
+ "version": "0.3.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.2.0"
23
+ "naive-framework": "^0.3.0"
24
24
  }
25
25
  }