create-venture 0.1.1 → 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.
- package/index.mjs +209 -30
- 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).
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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] [--
|
|
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
|
-
// [--framework <spec>] [--reset-preferences]
|
|
13
|
+
// [--framework <spec>] [--reset-preferences] [--venture-name "<name>"] [--no-workspace]
|
|
9
14
|
//
|
|
10
|
-
// --
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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", "layout", "venture-name"]);
|
|
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);
|
|
@@ -62,31 +75,137 @@ const choose = async (q, options, fallback) => {
|
|
|
62
75
|
return a;
|
|
63
76
|
};
|
|
64
77
|
|
|
65
|
-
// 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";
|
|
66
82
|
let target = positional[0];
|
|
83
|
+
let ventureName = opt("venture-name");
|
|
67
84
|
if (!target) {
|
|
68
|
-
|
|
69
|
-
target =
|
|
85
|
+
ventureName ??= interactive ? await ask("What is your venture named?", FIRST) : FIRST;
|
|
86
|
+
target = slug(ventureName);
|
|
70
87
|
}
|
|
71
88
|
const dir = resolve(target);
|
|
89
|
+
ventureName ??= basename(dir);
|
|
72
90
|
const id = slug(basename(dir));
|
|
73
91
|
if (!id) fail(`${basename(dir)} has no letters or digits to make a venture id from`);
|
|
74
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.`);
|
|
75
93
|
|
|
76
|
-
//
|
|
94
|
+
// The framework's registries: venture types, roles and functions (EE-082).
|
|
95
|
+
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 }));
|
|
96
|
+
const DEPARTMENTS = JSON.parse(readFileSync(join(framework, "operations/departments.json"), "utf8")).departments;
|
|
97
|
+
const deptLabel = (d) => DEPARTMENTS.find((x) => x.id === d)?.label ?? d;
|
|
98
|
+
const roleOf = (r) => ROLES.find((x) => x.id === r);
|
|
99
|
+
const gitName = () => spawnSync("git", ["config", "user.name"], { encoding: "utf8" }).stdout?.trim() || null;
|
|
100
|
+
|
|
101
|
+
// 2. What you are building: in a few words for the model, or from the list. Nothing is scaffolded until it is set.
|
|
102
|
+
let vtype = opt("type") ? TYPES.find((t) => t.id === opt("type")) : null;
|
|
103
|
+
if (opt("type") && !vtype) fail(`unknown venture type ${opt("type")}: one of ${TYPES.map((t) => t.id).join(", ")}`);
|
|
104
|
+
const pickFromList = async () => {
|
|
105
|
+
TYPES.forEach((t, i) => console.log(` ${String(i + 1).padStart(2)}. ${t.label}: ${t.description}`));
|
|
106
|
+
const a = await ask("Which kind of venture is it? (number or id)", "1");
|
|
107
|
+
const t = TYPES[Number(a) - 1] ?? TYPES.find((x) => x.id === a);
|
|
108
|
+
if (!t) { console.log(" choose a number from the list"); if (piped && !piped.length) fail("no answer left to choose with"); return pickFromList(); }
|
|
109
|
+
return t;
|
|
110
|
+
};
|
|
111
|
+
const classified = async (words) => {
|
|
112
|
+
if (!canClassify()) { console.log(" (no TYPESAFE_API_KEY, so pick from the list)"); return null; }
|
|
113
|
+
try {
|
|
114
|
+
const c = classify(words);
|
|
115
|
+
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; }
|
|
116
|
+
return TYPES.find((t) => t.id === c.best.id);
|
|
117
|
+
} catch (e) { console.log(` ${e.message}; pick from the list`); return null; }
|
|
118
|
+
};
|
|
119
|
+
if (!vtype && opt("describe")) {
|
|
120
|
+
vtype = await classified(opt("describe"));
|
|
121
|
+
if (!vtype && !interactive) fail(`could not classify "${opt("describe")}": pass --type, one of ${TYPES.map((t) => t.id).join(", ")}`);
|
|
122
|
+
if (vtype) console.log(` ${opt("describe")}: a ${vtype.label.toLowerCase()}`);
|
|
123
|
+
}
|
|
124
|
+
if (!vtype && interactive) {
|
|
125
|
+
const words = await ask("What are you building? Describe it in a few words, or press Enter to pick from the list", "");
|
|
126
|
+
if (words) {
|
|
127
|
+
const t = await classified(words);
|
|
128
|
+
if (t && (await choose(`That sounds like a ${t.label.toLowerCase()}. Is that right?`, ["yes", "no"], "yes")) === "yes") vtype = t;
|
|
129
|
+
}
|
|
130
|
+
if (!vtype) vtype = await pickFromList();
|
|
131
|
+
}
|
|
132
|
+
// --yes with no type: the recommended default, a software startup.
|
|
133
|
+
if (!vtype) vtype = TYPES.find((t) => t.id === "software-startup");
|
|
134
|
+
|
|
135
|
+
// 3. Your role in it, from the roles this type has. Investors are a role too: a general partner runs a fund.
|
|
136
|
+
let role = opt("role") ?? (interactive ? await choose("What is your role in it?", vtype.roles, vtype.default_role) : vtype.default_role);
|
|
137
|
+
if (!vtype.roles.includes(role)) fail(`a ${vtype.label.toLowerCase()} has no ${role} role: one of ${vtype.roles.join(", ")}`);
|
|
138
|
+
|
|
139
|
+
// 4. You, and whether you are building it solo or with a team.
|
|
140
|
+
const me = opt("name") ?? (interactive ? await ask("What is your name?", gitName() ?? "Founder") : gitName() ?? "Founder");
|
|
141
|
+
let others = opt("team") ? opt("team").split(",").map((x) => x.trim()).filter(Boolean) : [];
|
|
142
|
+
if (!opt("team") && interactive && (await choose("Are you building it solo or with a team?", ["solo", "team"], prefs?.mode ?? "solo")) === "team") {
|
|
143
|
+
others = (await ask("Who else is founding it? (names, comma separated)", "")).split(",").map((x) => x.trim()).filter(Boolean);
|
|
144
|
+
if (!others.length) console.log(" no other names, so you are building it solo");
|
|
145
|
+
}
|
|
146
|
+
const mode = others.length ? "team" : "solo";
|
|
147
|
+
const coRole = vtype.id === "vc-fund" ? "general-partner" : "co-founder";
|
|
148
|
+
const people = [{ id: slug(me) || "founder", name: me, role }, ...others.map((n) => ({ id: slug(n), name: n, role: coRole }))];
|
|
149
|
+
if (new Set(people.map((p) => p.id)).size !== people.length) fail("two people would have the same id; give each a distinct name");
|
|
150
|
+
|
|
151
|
+
// 5. The phase, the hats, and the rest of the defaults (I-3, I-7).
|
|
77
152
|
const recommended = { phase: "discovery", layers: null, legalForm: null, dump: null };
|
|
78
153
|
let answers = { ...recommended };
|
|
79
154
|
const given = opt("phase") || opt("layers") || opt("legal-form") || opt("dump");
|
|
80
155
|
if (given) answers = { phase: opt("phase") ?? "discovery", layers: opt("layers") ?? null, legalForm: opt("legal-form") ?? null, dump: opt("dump") ?? null };
|
|
81
|
-
else if (interactive)
|
|
156
|
+
else if (interactive) answers.phase = await choose("Which phase is the venture in?", PHASES, prefs?.phase ?? "discovery");
|
|
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));
|
|
166
|
+
const owners = people.filter((p) => roleOf(p.role)?.owns_when_solo);
|
|
167
|
+
const ownerOf = {};
|
|
168
|
+
for (const p of people) p.hats = [];
|
|
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);
|
|
174
|
+
}
|
|
175
|
+
// 6. Where each function keeps its truth (EE-083): pointers to its systems of record, in the founder's words when none fits.
|
|
176
|
+
const CATALOG = JSON.parse(readFileSync(join(framework, "schema/systems-catalog.json"), "utf8"));
|
|
177
|
+
const asked = [...vtype.departments.filter((d) => CATALOG.functions[d]), "legal"];
|
|
178
|
+
const parseSystems = (text) => {
|
|
179
|
+
const items = text.split(",").map((x) => x.trim()).filter(Boolean);
|
|
180
|
+
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; });
|
|
181
|
+
return systems.every(Boolean) ? { systems } : { systems: [], note: text.trim() };
|
|
182
|
+
};
|
|
183
|
+
const functions = {};
|
|
184
|
+
const sourceFlags = argv.flatMap((a, i) => (a === "--source" ? [argv[i + 1]] : []));
|
|
185
|
+
for (const f of sourceFlags) {
|
|
186
|
+
const [fn, ...rest] = f.split("=");
|
|
187
|
+
if (!CATALOG.functions[fn]) fail(`--source ${f}: ${fn} is not a function systems are kept for (${Object.keys(CATALOG.functions).join(", ")})`);
|
|
188
|
+
functions[fn] = parseSystems(rest.join("="));
|
|
189
|
+
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(", ")})`);
|
|
190
|
+
}
|
|
191
|
+
if (interactive && !sourceFlags.length) {
|
|
192
|
+
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.");
|
|
193
|
+
for (const fn of asked) {
|
|
194
|
+
const options = CATALOG.functions[fn];
|
|
195
|
+
const a = await ask(`Where is the source of truth for ${fn === "legal" ? "legal documents" : deptLabel(fn)}? ${options.join(" / ")} / none`, "none");
|
|
196
|
+
if (a.toLowerCase() === "none") continue;
|
|
197
|
+
functions[fn] = parseSystems(a);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (interactive && !given) {
|
|
82
202
|
const modes = prefs ? ["recommended", "previous", "customize"] : ["recommended", "customize"];
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
85
|
-
if (
|
|
86
|
-
|
|
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(", ")})`, "");
|
|
203
|
+
const pick = await choose("Would you like to use the recommended Naive defaults for the rest?", modes, "recommended");
|
|
204
|
+
if (pick === "previous") answers = { ...answers, layers: prefs.layers ?? null, legalForm: prefs.legalForm ?? null };
|
|
205
|
+
if (pick === "customize") {
|
|
206
|
+
const l = await ask(`Which layers should be on? (comma separated, from ${LAYERS.join(", ")}; empty for the defaults)`, "");
|
|
88
207
|
answers.layers = l || null;
|
|
89
|
-
const f = await choose("Is the venture a legal entity yet?",
|
|
208
|
+
const f = await choose("Is the venture a legal entity yet?", FORMS, "none");
|
|
90
209
|
answers.legalForm = f === "none" ? null : f;
|
|
91
210
|
const d = await ask("Paste the path of an idea dump to start from (optional)", "");
|
|
92
211
|
answers.dump = d || null;
|
|
@@ -94,6 +213,7 @@ else if (interactive) {
|
|
|
94
213
|
}
|
|
95
214
|
if (!PHASES.includes(answers.phase)) fail(`unknown phase ${answers.phase}: one of ${PHASES.join(", ")}`);
|
|
96
215
|
if (answers.dump && !existsSync(resolve(answers.dump))) fail(`no idea dump at ${answers.dump}`);
|
|
216
|
+
if (answers.legalForm && !FORMS.includes(answers.legalForm)) fail(`unknown legal form ${answers.legalForm}: one of ${FORMS.join(", ")}`);
|
|
97
217
|
|
|
98
218
|
// 3. The package manager: the flag, else whichever ran this (npm_config_user_agent), else npm.
|
|
99
219
|
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 +221,59 @@ const manager = ["npm", "pnpm", "yarn", "bun"].includes(pm) ? pm : "npm";
|
|
|
101
221
|
rl?.close();
|
|
102
222
|
|
|
103
223
|
// 4. The venture itself, by the framework's own scaffold.
|
|
104
|
-
console.log(`\nCreating a new
|
|
105
|
-
const init = spawnSync("node", [join(framework, "schema/init-venture.mjs"), dir, id, "--phase", answers.phase,
|
|
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`);
|
|
225
|
+
const init = spawnSync("node", [join(framework, "schema/init-venture.mjs"), dir, id, "--phase", answers.phase, "--type", vtype.id, "--layout", layout,
|
|
106
226
|
...(answers.layers ? ["--layers", answers.layers] : []), ...(answers.legalForm ? ["--legal-form", answers.legalForm] : [])], { encoding: "utf8" });
|
|
107
227
|
if (init.status !== 0) fail((init.stderr || init.stdout).trim());
|
|
108
228
|
const vfile = join(dir, "venture.json");
|
|
109
229
|
const venture = JSON.parse(readFileSync(vfile, "utf8"));
|
|
110
|
-
venture.name =
|
|
230
|
+
venture.name = ventureName;
|
|
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;
|
|
111
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"));
|
|
112
237
|
|
|
238
|
+
// people/: the roster and one file per founder, the venture.json of human capital (EE-082).
|
|
113
239
|
const { agentFromEnv, record } = await import(join(framework, "interface/provenance.mjs"));
|
|
240
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
241
|
+
mkdirSync(vf(dir, "people/founders"), { recursive: true });
|
|
242
|
+
const asPerson = agentFromEnv("person", "cli", { id: people[0].id });
|
|
243
|
+
for (const p of people.filter((x) => roleOf(x.role)?.founder)) {
|
|
244
|
+
p.founder_file = `people/founders/${p.id}.json`;
|
|
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");
|
|
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` });
|
|
247
|
+
}
|
|
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");
|
|
249
|
+
record(dir, { entity: "people/people.json", route: "people", activity: "write", agent: asPerson, note: `${mode}: ${people.map((p) => `${p.name} (${p.role})`).join(", ")}` });
|
|
114
250
|
if (answers.dump) {
|
|
115
|
-
mkdirSync(
|
|
116
|
-
copyFileSync(resolve(answers.dump),
|
|
117
|
-
record(dir, { entity: "raw/dump.md", route: "raw/dump", activity: "
|
|
251
|
+
mkdirSync(vf(dir, "raw"), { recursive: true });
|
|
252
|
+
copyFileSync(resolve(answers.dump), vf(dir, "raw/dump.md"));
|
|
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)}` });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// knowledge/sources.json: each function's systems of record, as pointers (EE-083). Rho is read by code, so it is synced now.
|
|
257
|
+
const rhoScript = join(framework, "platform/integrations/rho.mjs");
|
|
258
|
+
let rhoNote = null;
|
|
259
|
+
for (const f of Object.values(functions)) for (const s of f.systems) {
|
|
260
|
+
s.status = "pointed";
|
|
261
|
+
if (s.system !== "rho") continue;
|
|
262
|
+
if (!existsSync(rhoScript)) { rhoNote = "Rho: recorded as pointed; this copy of Naive has no Rho integration"; continue; }
|
|
263
|
+
const fixture = flag("rho-fixture") ? join(framework, "platform/integrations/fixtures/rho-sandbox.json") : null;
|
|
264
|
+
const live = !fixture && process.env.RHO_API_TOKEN;
|
|
265
|
+
const r = spawnSync("node", [rhoScript, "sync", "--venture", dir, ...(fixture ? ["--fixture", fixture] : live ? ["--live"] : [])], { encoding: "utf8", env: process.env });
|
|
266
|
+
if (r.status === 0) {
|
|
267
|
+
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") });
|
|
268
|
+
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/`;
|
|
269
|
+
} else rhoNote = `Rho: could not sync (${(r.stderr || r.stdout).trim().split("\n").at(-1)}); recorded as pointed`;
|
|
270
|
+
}
|
|
271
|
+
if (Object.keys(functions).length) {
|
|
272
|
+
const map = vf(dir, "knowledge/sources.json");
|
|
273
|
+
mkdirSync(dirname(map), { recursive: true });
|
|
274
|
+
writeFileSync(map, JSON.stringify({ sources: [], records: {}, truth: "local", functions }, null, 2) + "\n");
|
|
275
|
+
record(dir, { entity: "knowledge/sources.json", route: "knowledge/knowledge-sources", activity: "write", agent: asPerson,
|
|
276
|
+
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
277
|
}
|
|
119
278
|
|
|
120
279
|
// The venture's package.json: the framework as a dev dependency and the four scripts (I-17).
|
|
@@ -126,12 +285,12 @@ writeFileSync(join(dir, "package.json"), JSON.stringify({
|
|
|
126
285
|
scripts: { dev: "naive dev", build: "naive build", start: "naive start", check: "naive check" },
|
|
127
286
|
devDependencies: { [fw.name]: spec },
|
|
128
287
|
}, 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");
|
|
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");
|
|
130
289
|
|
|
131
290
|
// Editor completion for records (I-26): every record file is mapped to its JSON Schema in the installed framework.
|
|
132
291
|
const { routes } = await import(join(framework, "interface/router.mjs"));
|
|
133
292
|
const schemas = new Map();
|
|
134
|
-
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)}`]);
|
|
135
294
|
mkdirSync(join(dir, ".vscode"), { recursive: true });
|
|
136
295
|
writeFileSync(join(dir, ".vscode/settings.json"), JSON.stringify({
|
|
137
296
|
"json.schemas": [...schemas].map(([schema, fileMatch]) => ({ fileMatch: [...new Set(fileMatch)], url: `./node_modules/${fw.name}/${schema}` })),
|
|
@@ -151,13 +310,33 @@ if (!flag("no-git") && spawnSync("git", ["--version"]).status === 0 && spawnSync
|
|
|
151
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";
|
|
152
311
|
} else if (!flag("no-git")) git = "skipped (already inside a git repository)";
|
|
153
312
|
|
|
154
|
-
|
|
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
|
+
|
|
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 {}
|
|
155
332
|
|
|
156
333
|
const runScript = (s) => (manager === "npm" ? `npm run ${s}` : `${manager} ${s}`);
|
|
157
334
|
const cd = relative(process.cwd(), dir) || ".";
|
|
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.";
|
|
158
336
|
console.log(`
|
|
159
337
|
${bold("Success!")} Created ${basename(dir)} at ${dir}
|
|
160
|
-
|
|
338
|
+
Workspace: ${workspace ? `${workspace.name} at ${workspace.root} (${WORKSPACE_FILE})` : "none (--no-workspace)"}
|
|
339
|
+
Git: ${git}${rhoNote ? `\n${rhoNote}` : ""}${legal ? `\n${legal}` : ""}
|
|
161
340
|
|
|
162
341
|
Inside that directory, you can run:
|
|
163
342
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-venture",
|
|
3
|
-
"version": "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.
|
|
23
|
+
"naive-framework": "^0.3.0"
|
|
24
24
|
}
|
|
25
25
|
}
|