partweave 0.3.1 → 0.3.2

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.
@@ -4,8 +4,80 @@
4
4
  import { spawnSync as spawnSync3 } from "child_process";
5
5
  import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
6
6
  import { basename, resolve as resolve3 } from "path";
7
- import { confirm as confirm3, intro, isCancel as isCancel3, log as log2, note, outro, spinner as spinner2 } from "@clack/prompts";
8
- import pc2 from "picocolors";
7
+ import { confirm as confirm3, intro, isCancel as isCancel3, log as log2, note as note2, outro, spinner as spinner2 } from "@clack/prompts";
8
+ import pc3 from "picocolors";
9
+
10
+ // src/banner.ts
11
+ import pc from "picocolors";
12
+ var WEAVE = ["\u2588", "\u2593"];
13
+ var weaveCell = (row, col) => WEAVE[(row + col) % 2];
14
+ var GLYPHS = {
15
+ p: ["\u2588\u2588\u2588 ", "\u2588 \u2588", "\u2588\u2588\u2588 ", "\u2588 ", "\u2588 "],
16
+ a: [" \u2588\u2588 ", "\u2588 \u2588", "\u2588\u2588\u2588\u2588", "\u2588 \u2588", "\u2588 \u2588"],
17
+ r: ["\u2588\u2588\u2588 ", "\u2588 \u2588", "\u2588\u2588\u2588 ", "\u2588 \u2588 ", "\u2588 \u2588"],
18
+ t: ["\u2588\u2588\u2588\u2588\u2588", " \u2588 ", " \u2588 ", " \u2588 ", " \u2588 "],
19
+ w: ["\u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588 \u2588", "\u2588\u2588 \u2588\u2588", "\u2588 \u2588"],
20
+ e: ["\u2588\u2588\u2588\u2588", "\u2588 ", "\u2588\u2588\u2588 ", "\u2588 ", "\u2588\u2588\u2588\u2588"],
21
+ v: ["\u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588", " \u2588 \u2588 ", " \u2588 "]
22
+ };
23
+ var WORD = "partweave";
24
+ var HEIGHT = 5;
25
+ function colorMode() {
26
+ if (process.env.NO_COLOR || !process.stdout.isTTY) return "none";
27
+ if (/truecolor|24bit/i.test(process.env.COLORTERM ?? "")) return "truecolor";
28
+ return "basic";
29
+ }
30
+ var STOPS = [
31
+ [124, 92, 246],
32
+ // violet
33
+ [190, 75, 219],
34
+ // magenta
35
+ [232, 74, 143],
36
+ // pink
37
+ [225, 51, 74],
38
+ // red
39
+ [248, 96, 76]
40
+ // coral / bright red
41
+ ];
42
+ function gradient(t) {
43
+ const n = STOPS.length - 1;
44
+ const scaled = Math.min(Math.max(t, 0), 1) * n;
45
+ const i = Math.min(Math.floor(scaled), n - 1);
46
+ const local = scaled - i;
47
+ const [a, b] = [STOPS[i], STOPS[i + 1]];
48
+ return [0, 1, 2].map((k) => Math.round(a[k] + (b[k] - a[k]) * local));
49
+ }
50
+ function paint(ch, t, mode) {
51
+ if (ch === " ") return " ";
52
+ if (mode === "none") return ch;
53
+ if (mode === "basic") return t < 0.5 ? pc.magentaBright(ch) : pc.redBright(ch);
54
+ const [r, g, b] = gradient(t);
55
+ return `\x1B[38;2;${r};${g};${b}m${ch}\x1B[39m`;
56
+ }
57
+ function compact(mode) {
58
+ return " " + (mode === "none" ? "partweave" : word2tone("partweave", mode));
59
+ }
60
+ function word2tone(word, mode) {
61
+ if (mode === "none") return word;
62
+ return [...word].map((ch, i) => paint(ch, word.length > 1 ? i / (word.length - 1) : 0, mode)).join("");
63
+ }
64
+ function renderBanner() {
65
+ const mode = colorMode();
66
+ if (!process.stdout.isTTY) return compact(mode);
67
+ const cols = process.stdout.columns ?? 80;
68
+ const rows = [];
69
+ for (let r = 0; r < HEIGHT; r++) {
70
+ rows.push([...WORD].map((ch) => GLYPHS[ch]?.[r] ?? "").join(" "));
71
+ }
72
+ const width = rows[0].length;
73
+ if (width + 2 > cols) return compact(mode);
74
+ const painted = rows.map(
75
+ (row, r) => " " + [...row].map(
76
+ (ch, c) => ch === " " ? " " : paint(weaveCell(r, c), width > 1 ? c / (width - 1) : 0, mode)
77
+ ).join("")
78
+ );
79
+ return painted.join("\n");
80
+ }
9
81
 
10
82
  // src/compose.ts
11
83
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
@@ -26,14 +98,19 @@ function injectAtAnchor(content, anchorId, lines) {
26
98
  throw new Error(`Anchor <partweave:${anchorId}> not found in target file`);
27
99
  }
28
100
  const indent = match[1] ?? "";
29
- const existing = new Set(
30
- content.split("\n").map((l) => l.trim()).filter(Boolean)
31
- );
32
- const toInsert = lines.filter((l) => !existing.has(l.trim()));
101
+ const before = content.slice(0, match.index).split("\n");
102
+ if (before[before.length - 1] === "") before.pop();
103
+ const block = /* @__PURE__ */ new Set();
104
+ for (let i = before.length - 1; i >= 0; i--) {
105
+ const trimmed = before[i].trim();
106
+ if (trimmed === "") break;
107
+ block.add(trimmed);
108
+ }
109
+ const toInsert = lines.filter((l) => !block.has(l.trim()));
33
110
  if (toInsert.length === 0) return { content, inserted: 0 };
34
- const block = toInsert.map((l) => l ? indent + l : "").join("\n") + "\n";
111
+ const insertion = toInsert.map((l) => l ? indent + l : "").join("\n") + "\n";
35
112
  const anchorStart = match.index;
36
- const newContent = content.slice(0, anchorStart) + block + content.slice(anchorStart);
113
+ const newContent = content.slice(0, anchorStart) + insertion + content.slice(anchorStart);
37
114
  return { content: newContent, inserted: toInsert.length };
38
115
  }
39
116
  function parseDep(dep) {
@@ -50,34 +127,35 @@ function normalizeWorkspaceDeps(deps, workspaceRange) {
50
127
  return dep;
51
128
  });
52
129
  }
130
+ function higherVersion(a, b) {
131
+ const core = (v) => {
132
+ const m = v.match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
133
+ if (!m || m[1] === void 0) return null;
134
+ return [m[1], m[2], m[3]].map((n) => n === void 0 ? 0 : Number(n));
135
+ };
136
+ const ca = core(a);
137
+ const cb = core(b);
138
+ if (!ca || !cb) return a;
139
+ for (let i = 0; i < 3; i++) {
140
+ if (ca[i] !== cb[i]) return ca[i] > cb[i] ? a : b;
141
+ }
142
+ return a;
143
+ }
53
144
  function mergePackageJsonDeps(pkgJson, deps, field = "dependencies") {
54
145
  if (deps.length === 0) return pkgJson;
55
146
  const pkg = JSON.parse(pkgJson);
56
147
  const bucket = pkg[field] ?? {};
57
148
  for (const dep of deps) {
58
149
  const { name, version } = parseDep(dep);
59
- if (!(name in bucket)) bucket[name] = version;
150
+ bucket[name] = name in bucket ? higherVersion(bucket[name], version) : version;
60
151
  }
61
152
  pkg[field] = Object.fromEntries(
62
153
  Object.entries(bucket).sort(([a], [b]) => a.localeCompare(b))
63
154
  );
64
155
  return JSON.stringify(pkg, null, 2) + "\n";
65
156
  }
66
- function appendEnv(envBody, entries, heading) {
67
- const keys = Object.keys(entries);
68
- if (keys.length === 0) return envBody;
69
- const present = new Set(
70
- envBody.split("\n").map((l) => l.split("=")[0]?.trim()).filter(Boolean)
71
- );
72
- const missing = keys.filter((k) => !present.has(k));
73
- if (missing.length === 0) return envBody;
74
- let out = envBody.replace(/\n*$/, "\n");
75
- if (heading) out += `
76
- # ${heading}
77
- `;
78
- for (const k of missing) out += `${k}=${entries[k]}
79
- `;
80
- return out;
157
+ function pyDepName(dep) {
158
+ return dep.trim().replace(/^["']|["']$/g, "").split(/[<>=!~;[\s]/)[0].trim().toLowerCase();
81
159
  }
82
160
 
83
161
  // src/fsutil.ts
@@ -548,45 +626,82 @@ function buildTsconfigBase(ctx) {
548
626
  2
549
627
  ) + "\n";
550
628
  }
551
- function buildBaseEnv(ctx) {
552
- const lines = [
553
- "# Environment for " + ctx.projectName,
554
- "#",
555
- "# Every value the app reads from the environment lives here. Copy this file to",
556
- "# `.env` and adjust as needed \u2014 the server, web/mobile apps, and each component",
557
- "# read their configuration from it, so there's nothing to configure elsewhere.",
558
- ""
559
- ];
629
+ function envScopeFor(key) {
630
+ if (key.startsWith("POSTGRES_")) return "root";
631
+ if (key.startsWith("NEXT_PUBLIC_")) return "web";
632
+ if (key.startsWith("EXPO_PUBLIC_")) return "mobile";
633
+ return "server";
634
+ }
635
+ function buildEnvFiles(ctx, modules) {
636
+ const componentBlocks = { server: [], web: [], mobile: [], root: [] };
637
+ for (const m of modules) {
638
+ const byScope = { server: [], web: [], mobile: [], root: [] };
639
+ for (const [key, value] of Object.entries(m.manifest.env)) {
640
+ byScope[envScopeFor(key)].push(`${key}=${value}`);
641
+ }
642
+ for (const scope of ["server", "web", "mobile", "root"]) {
643
+ if (byScope[scope].length === 0) continue;
644
+ componentBlocks[scope].push(`# ${m.manifest.title}`, ...byScope[scope], "");
645
+ }
646
+ }
647
+ const render2 = (lines) => lines.join("\n").replace(/\n*$/, "\n");
648
+ const files = [];
560
649
  if (ctx.hasServer) {
561
- lines.push(
562
- "# --- server ---",
563
- "# Unique per-project key generated at scaffold time; signs sessions and JWTs.",
564
- "# Use a separate secret value in production (and never commit the real one).",
565
- `DJANGO_SECRET_KEY=${randomBytes(48).toString("base64url")}`,
650
+ const serverBase = (secretLine) => [
651
+ `# ${ctx.projectName} \u2014 server (Django). Read by apps/server.`,
652
+ "# `.env` is gitignored; keep real secrets here, not in `.env.example`.",
653
+ "",
654
+ "# Unique per-project key; signs sessions and JWTs.",
655
+ secretLine,
566
656
  "DJANGO_DEBUG=true",
567
- "DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1",
568
- "# Web origins allowed to call the API when DEBUG is off (comma-separated).",
569
- "# In DEBUG every origin is allowed, so this is only needed in production.",
657
+ "# Leave DJANGO_ALLOWED_HOSTS unset in dev: with DEBUG on, any host is allowed,",
658
+ "# so a phone/simulator can reach the server over your LAN (e.g. 192.168.x.y).",
659
+ "# Set it (comma-separated) in production, e.g. DJANGO_ALLOWED_HOSTS=api.example.com",
660
+ "# Origins allowed to call the API when DEBUG is off (comma-separated).",
570
661
  "# DJANGO_CORS_ALLOWED_ORIGINS=https://app.example.com",
571
- "# DATABASE_URL \u2014 the server uses local SQLite by default; set a database URL",
572
- "# to switch (the `db-postgres` component sets a Postgres DSN here for you).",
662
+ "# DATABASE_URL \u2014 unset uses local SQLite; the db-postgres component sets a Postgres DSN.",
573
663
  ""
574
- );
664
+ ];
665
+ files.push({
666
+ dir: "apps/server",
667
+ example: render2([...serverBase("DJANGO_SECRET_KEY=replace-with-a-generated-secret"), ...componentBlocks.server]),
668
+ env: render2([...serverBase(`DJANGO_SECRET_KEY=${randomBytes(48).toString("base64url")}`), ...componentBlocks.server])
669
+ });
575
670
  }
576
671
  if (ctx.hasWeb) {
577
- lines.push("# --- web ---", "NEXT_PUBLIC_API_URL=http://localhost:8000", "");
672
+ const web = [
673
+ `# ${ctx.projectName} \u2014 web (Next.js). Read by apps/web.`,
674
+ "# Only NEXT_PUBLIC_* is exposed to the browser.",
675
+ "NEXT_PUBLIC_API_URL=http://localhost:8000",
676
+ "",
677
+ ...componentBlocks.web
678
+ ];
679
+ const body = render2(web);
680
+ files.push({ dir: "apps/web", example: body, env: body });
578
681
  }
579
682
  if (ctx.hasMobile) {
580
- lines.push(
581
- "# --- mobile (Expo) ---",
582
- "# In dev the app auto-detects your machine's LAN IP so a physical device can",
583
- "# reach the server \u2014 leave this unset. Set it for simulators or production,",
584
- "# in the environment or apps/mobile/.env (Expo reads EXPO_PUBLIC_* from there).",
683
+ const mobile = [
684
+ `# ${ctx.projectName} \u2014 mobile (Expo). Read by apps/mobile.`,
685
+ "# Only EXPO_PUBLIC_* is exposed to the app. In dev the app auto-detects your",
686
+ "# machine's LAN IP, so this can stay unset; set it for simulators/device/production.",
585
687
  "# EXPO_PUBLIC_API_URL=http://localhost:8000",
586
- ""
587
- );
588
- }
589
- return lines.join("\n");
688
+ "",
689
+ ...componentBlocks.mobile
690
+ ];
691
+ const body = render2(mobile);
692
+ files.push({ dir: "apps/mobile", example: body, env: body });
693
+ }
694
+ if (componentBlocks.root.length > 0) {
695
+ const root = [
696
+ `# ${ctx.projectName} \u2014 infrastructure. Read by docker compose (--env-file .env).`,
697
+ "# Keep these in sync with DATABASE_URL in apps/server/.env.",
698
+ "",
699
+ ...componentBlocks.root
700
+ ];
701
+ const body = render2(root);
702
+ files.push({ dir: "", example: body, env: body });
703
+ }
704
+ return files;
590
705
  }
591
706
  function jsCiSteps(ctx) {
592
707
  if (ctx.jsPm === "npm") {
@@ -711,7 +826,18 @@ function buildReadme(ctx, modules) {
711
826
  if (ctx.hasWeb) parts.push("npm run web # http://localhost:3000");
712
827
  if (ctx.hasMobile) parts.push("npm run mobile # Expo dev server");
713
828
  parts.push("```", "");
714
- parts.push("Copy `.env.example` to `.env` and fill in values before running.", "");
829
+ parts.push("## Configuration", "");
830
+ parts.push(
831
+ "Each app reads its **own** env file, created for you (and gitignored) with a",
832
+ "committed `.env.example` template alongside it. Edit the `.env` to change values:",
833
+ ""
834
+ );
835
+ const envFiles = [];
836
+ if (ctx.hasServer) envFiles.push("- `apps/server/.env` \u2014 Django secret key, allowed hosts, `DATABASE_URL`");
837
+ if (ctx.hasWeb) envFiles.push("- `apps/web/.env` \u2014 `NEXT_PUBLIC_*` (browser-exposed)");
838
+ if (ctx.hasMobile) envFiles.push("- `apps/mobile/.env` \u2014 `EXPO_PUBLIC_*` (app-exposed)");
839
+ if (hasDocker) envFiles.push("- `.env` (root) \u2014 `POSTGRES_*` for the database container");
840
+ parts.push(...envFiles, "");
715
841
  return parts.join("\n");
716
842
  }
717
843
  function buildServerDockerfile(ctx) {
@@ -838,7 +964,7 @@ var ManifestSchema = z.object({
838
964
  conflicts: z.array(z.string()).default([]),
839
965
  /** capability/interface this module satisfies (for conflict grouping) */
840
966
  provides: z.string().optional(),
841
- /** env keys → default value, appended to .env.example */
967
+ /** env keys → default value; routed to the consuming app's .env/.env.example by prefix (POSTGRES_→root, NEXT_PUBLIC_→web, EXPO_PUBLIC_→mobile, else server) */
842
968
  env: z.record(z.string()).default({}),
843
969
  /** per-target wiring (files are copied separately from wiring injection) */
844
970
  wiring: z.record(z.enum(TARGETS), WiringForTargetSchema).default({}),
@@ -928,6 +1054,25 @@ function injectIntoFiles(index, anchorId, lines, targetLabel) {
928
1054
  writeFileSync2(file, next);
929
1055
  }
930
1056
  }
1057
+ function injectPyDeps(index, deps, targetLabel) {
1058
+ const files = index.get("deps");
1059
+ if (!files || files.length === 0) {
1060
+ throw new Error(
1061
+ `Wiring error: anchor <partweave:deps> not found in ${targetLabel}. Add the anchor to the _core/${targetLabel} scaffold.`
1062
+ );
1063
+ }
1064
+ for (const file of files) {
1065
+ const content = readFileSync2(file, "utf8");
1066
+ const region = content.match(/dependencies\s*=\s*\[([\s\S]*?)\]/)?.[1] ?? content;
1067
+ const present = new Set(
1068
+ [...region.matchAll(/["']([^"']+)["']/g)].map((m) => pyDepName(m[1]))
1069
+ );
1070
+ const fresh = deps.filter((d) => !present.has(pyDepName(d)));
1071
+ if (fresh.length === 0) continue;
1072
+ const { content: next } = injectAtAnchor(content, "deps", fresh.map((d) => `"${d}",`));
1073
+ writeFileSync2(file, next);
1074
+ }
1075
+ }
931
1076
  function applyWiring(outDir, targets, modules, workspaceRange) {
932
1077
  const indexes = /* @__PURE__ */ new Map();
933
1078
  for (const t of targets) {
@@ -945,8 +1090,7 @@ function applyWiring(outDir, targets, modules, workspaceRange) {
945
1090
  }
946
1091
  if (wiring.deps?.length) {
947
1092
  if (t === "server") {
948
- const lines = wiring.deps.map((d) => `"${d}",`);
949
- injectIntoFiles(index, "deps", lines, t);
1093
+ injectPyDeps(index, wiring.deps, t);
950
1094
  } else {
951
1095
  const pkgPath = join2(targetDir, "package.json");
952
1096
  if (existsSync2(pkgPath)) {
@@ -961,14 +1105,19 @@ function applyWiring(outDir, targets, modules, workspaceRange) {
961
1105
  }
962
1106
  }
963
1107
  }
964
- function applyEnv(outDir, modules) {
965
- const envPath = join2(outDir, ".env.example");
966
- let body = existsSync2(envPath) ? readFileSync2(envPath, "utf8") : "";
967
- for (const mod of modules) {
968
- if (Object.keys(mod.manifest.env).length === 0) continue;
969
- body = appendEnv(body, mod.manifest.env, mod.manifest.title);
1108
+ function writeEnvFiles(outDir, ctx, modules) {
1109
+ const written = [];
1110
+ for (const file of buildEnvFiles(ctx, modules)) {
1111
+ const exampleRel = join2(file.dir, ".env.example");
1112
+ writeFileEnsured(join2(outDir, exampleRel), file.example);
1113
+ written.push(exampleRel);
1114
+ const envRel = join2(file.dir, ".env");
1115
+ if (!existsSync2(join2(outDir, envRel))) {
1116
+ writeFileEnsured(join2(outDir, envRel), file.env);
1117
+ written.push(envRel);
1118
+ }
970
1119
  }
971
- if (body) writeFileSync2(envPath, body);
1120
+ return written;
972
1121
  }
973
1122
  function writeStructuralRootFiles(outDir, ctx, modules) {
974
1123
  const workspace = buildJsWorkspace(ctx);
@@ -984,7 +1133,6 @@ function writeStructuralRootFiles(outDir, ctx, modules) {
984
1133
  if (pipSync) writeFileEnsured(join2(outDir, "apps/server/scripts/sync_deps.py"), pipSync);
985
1134
  writeFileEnsured(join2(outDir, "scripts/run.mjs"), buildTaskRunner(ctx, hasDocker));
986
1135
  writeFileEnsured(join2(outDir, "Makefile"), buildMakefile(ctx, hasDocker));
987
- writeFileEnsured(join2(outDir, ".env.example"), buildBaseEnv(ctx));
988
1136
  }
989
1137
  function compose(opts) {
990
1138
  const { selection, registry, scaffoldTargets, wireTargets, rootFiles } = opts;
@@ -1013,7 +1161,7 @@ function compose(opts) {
1013
1161
  }
1014
1162
  }
1015
1163
  applyWiring(outDir, wireTargets, modules, jsPmProfile(ctx.jsPm).workspaceRange);
1016
- applyEnv(outDir, modules);
1164
+ if (rootFiles !== "none") written.push(...writeEnvFiles(outDir, ctx, modules));
1017
1165
  if (selection.modules.includes("ci")) {
1018
1166
  for (const [rel, content] of Object.entries(buildCiWorkflows(ctx))) {
1019
1167
  writeFileEnsured(join2(outDir, rel), content);
@@ -1209,11 +1357,28 @@ function resolveModules(registry, chosen) {
1209
1357
  provided.set(cap, id);
1210
1358
  }
1211
1359
  }
1212
- const order = [...resolved].sort((a, b) => {
1213
- const da = registry.require(a).manifest.requires.length;
1214
- const db = registry.require(b).manifest.requires.length;
1215
- return da - db || a.localeCompare(b);
1216
- });
1360
+ const ids = [...resolved];
1361
+ const deps = new Map(
1362
+ ids.map((id) => [id, registry.require(id).manifest.requires.filter((r) => resolved.has(r))])
1363
+ );
1364
+ const indegree = new Map(ids.map((id) => [id, deps.get(id).length]));
1365
+ const dependents = new Map(ids.map((id) => [id, []]));
1366
+ for (const id of ids) for (const d of deps.get(id)) dependents.get(d).push(id);
1367
+ const byId = (a, b) => a.localeCompare(b);
1368
+ const ready = ids.filter((id) => indegree.get(id) === 0).sort(byId);
1369
+ const order = [];
1370
+ while (ready.length > 0) {
1371
+ const id = ready.shift();
1372
+ order.push(id);
1373
+ for (const dep of dependents.get(id)) {
1374
+ const n = indegree.get(dep) - 1;
1375
+ indegree.set(dep, n);
1376
+ if (n === 0) {
1377
+ ready.push(dep);
1378
+ ready.sort(byId);
1379
+ }
1380
+ }
1381
+ }
1217
1382
  return { modules: order, autoAdded: [...autoAdded].sort() };
1218
1383
  }
1219
1384
  function validateApps(registry, moduleIds, apps) {
@@ -1242,12 +1407,13 @@ import {
1242
1407
  confirm as confirm2,
1243
1408
  isCancel as isCancel2,
1244
1409
  multiselect,
1410
+ note,
1245
1411
  select,
1246
1412
  text
1247
1413
  } from "@clack/prompts";
1248
- import pc from "picocolors";
1414
+ import pc2 from "picocolors";
1249
1415
  import { resolve as resolve2 } from "path";
1250
- var MULTI_HINT = pc.dim("\u2191/\u2193 move \xB7 space toggle \xB7 a all \xB7 enter confirm");
1416
+ var MULTI_HINT = pc2.dim("\u2191/\u2193 move \xB7 space toggle \xB7 a all \xB7 enter confirm");
1251
1417
  function bail(v) {
1252
1418
  if (isCancel2(v)) {
1253
1419
  cancel("Cancelled.");
@@ -1327,9 +1493,19 @@ async function promptCreate(registry, defaults) {
1327
1493
  bail(pyRaw);
1328
1494
  pyPm = pyRaw;
1329
1495
  }
1330
- const ok = await confirm2({
1331
- message: `Scaffold ${apps.join(" + ")}${modules.length ? " with " + modules.join(", ") : ""} into ${outDir}?`
1332
- });
1496
+ const row = (k, v) => `${pc2.dim(k.padEnd(8))} ${v}`;
1497
+ const tooling = [anyJs ? jsPm : null, apps.includes("server") ? pyPm : null].filter(Boolean).join(pc2.dim(" \xB7 "));
1498
+ note(
1499
+ [
1500
+ row("project", pc2.bold(projectName)),
1501
+ row("where", outDir),
1502
+ row("apps", apps.join(pc2.dim(" \xB7 "))),
1503
+ row("add-ons", modules.length ? modules.join(pc2.dim(" \xB7 ")) : pc2.dim("none")),
1504
+ row("tooling", tooling)
1505
+ ].join("\n"),
1506
+ "Review"
1507
+ );
1508
+ const ok = await confirm2({ message: "Scaffold this?" });
1333
1509
  bail(ok);
1334
1510
  if (!ok) {
1335
1511
  cancel("Cancelled.");
@@ -1339,6 +1515,21 @@ async function promptCreate(registry, defaults) {
1339
1515
  }
1340
1516
 
1341
1517
  // src/commands/create.ts
1518
+ function isInsideGitRepo(dir) {
1519
+ return spawnSync3("git", ["rev-parse", "--is-inside-work-tree"], { cwd: dir, stdio: "ignore" }).status === 0;
1520
+ }
1521
+ function initGit(dir) {
1522
+ if (spawnSync3("git", ["init", "-b", "main"], { cwd: dir, stdio: "ignore" }).status !== 0) {
1523
+ spawnSync3("git", ["init"], { cwd: dir, stdio: "ignore" });
1524
+ spawnSync3("git", ["branch", "-M", "main"], { cwd: dir, stdio: "ignore" });
1525
+ }
1526
+ spawnSync3("git", ["add", "-A"], { cwd: dir, stdio: "ignore" });
1527
+ const msg = "Initial commit (scaffolded with partweave)";
1528
+ const opts = { cwd: dir, stdio: "ignore" };
1529
+ if (spawnSync3("git", ["commit", "-m", msg], opts).status === 0) return true;
1530
+ const id = ["-c", "user.name=partweave", "-c", "user.email=partweave@users.noreply.github.com"];
1531
+ return spawnSync3("git", [...id, "commit", "-m", msg], opts).status === 0;
1532
+ }
1342
1533
  function resolvePm(value, allowed, detect, flag) {
1343
1534
  if (value === void 0) return detect();
1344
1535
  if (allowed.includes(value)) return value;
@@ -1360,7 +1551,8 @@ function defaultModules(registry, apps) {
1360
1551
  return registry.features().filter((m) => m.manifest.default).filter((m) => m.manifest.targets.some((t) => present.has(t))).filter((m) => m.manifest.requiresApps.every((a) => apps.includes(a))).map((m) => m.manifest.id);
1361
1552
  }
1362
1553
  async function runCreate(flags) {
1363
- intro(pc2.bgCyan(pc2.black(" partweave ")) + pc2.dim(" full-stack scaffolder"));
1554
+ console.log("\n" + renderBanner());
1555
+ intro(pc3.dim("full-stack scaffolder \u2014 pick the parts, own the code"));
1364
1556
  const registry = new Registry();
1365
1557
  const flagApps = appsFromFlags(flags);
1366
1558
  const nonInteractive = flags.yes === true || flagApps !== null;
@@ -1461,6 +1653,27 @@ async function runCreate(flags) {
1461
1653
  installed = false;
1462
1654
  }
1463
1655
  }
1656
+ let gitInitialized = false;
1657
+ const gitAvailable = hasCommand("git");
1658
+ const alreadyRepo = gitAvailable && isInsideGitRepo(choices.outDir);
1659
+ let wantGit;
1660
+ if (flags.git !== void 0) {
1661
+ wantGit = flags.git;
1662
+ } else if (nonInteractive) {
1663
+ wantGit = false;
1664
+ } else {
1665
+ const ans = await confirm3({ message: "Initialize a git repository?" });
1666
+ wantGit = !isCancel3(ans) && ans === true;
1667
+ }
1668
+ if (wantGit && gitAvailable && !alreadyRepo) {
1669
+ gitInitialized = initGit(choices.outDir);
1670
+ if (gitInitialized) log2.success("Initialized a git repository (branch main, initial commit).");
1671
+ else log2.warn("Couldn't create the initial git commit \u2014 the repo was left uninitialized.");
1672
+ } else if (wantGit && !gitAvailable) {
1673
+ log2.warn("git isn't installed \u2014 skipped repository initialization.");
1674
+ } else if (wantGit && alreadyRepo) {
1675
+ log2.info("Target is already inside a git repository \u2014 skipped `git init`.");
1676
+ }
1464
1677
  const rel = basename(choices.outDir);
1465
1678
  const hasDocker = resolved.modules.includes("docker");
1466
1679
  const steps = [`cd ${rel}`];
@@ -1471,12 +1684,12 @@ async function runCreate(flags) {
1471
1684
  }
1472
1685
  if (choices.apps.includes("web")) steps.push("npm run web");
1473
1686
  if (choices.apps.includes("mobile")) steps.push("npm run mobile");
1474
- note(
1475
- steps.join("\n") + "\n\n" + pc2.dim("These work on macOS, Linux & Windows. On macOS/Linux, `make <task>` works too."),
1687
+ note2(
1688
+ steps.join("\n") + "\n\n" + pc3.dim("These work on macOS, Linux & Windows. On macOS/Linux, `make <task>` works too."),
1476
1689
  "Next steps"
1477
1690
  );
1478
- if (result.notes.length) note(result.notes.join("\n"), "Notes");
1479
- outro(pc2.green("Done."));
1691
+ if (result.notes.length) note2(result.notes.join("\n"), "Notes");
1692
+ outro(pc3.green("Done."));
1480
1693
  }
1481
1694
 
1482
1695
  export {
@@ -1496,4 +1709,4 @@ export {
1496
1709
  validateApps,
1497
1710
  runCreate
1498
1711
  };
1499
- //# sourceMappingURL=chunk-ANKGMR2Z.js.map
1712
+ //# sourceMappingURL=chunk-Y5FZVVV6.js.map
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCreate
4
- } from "./chunk-ANKGMR2Z.js";
4
+ } from "./chunk-Y5FZVVV6.js";
5
5
 
6
6
  // src/create-bin.ts
7
7
  import { Command } from "commander";
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  selectedTargets,
16
16
  validateApps,
17
17
  writeProjectManifest
18
- } from "./chunk-ANKGMR2Z.js";
18
+ } from "./chunk-Y5FZVVV6.js";
19
19
 
20
20
  // src/index.ts
21
21
  import { Command } from "commander";
@@ -178,7 +178,7 @@ async function runDoctor(flags) {
178
178
  function buildProgram() {
179
179
  const program = new Command();
180
180
  program.name("partweave").description("A modular full-stack scaffolder \u2014 pick parts, generate only that code.").version("0.1.0");
181
- program.command("create", { isDefault: true }).argument("[name]", "project name").description("Scaffold a new project").option("-d, --dir <path>", "target directory").option("--server", "include the Django server").option("--no-server", "exclude the server").option("--web", "include the Next.js web app").option("--no-web", "exclude the web app").option("--mobile", "include the Expo mobile app").option("--no-mobile", "exclude the mobile app").option("--with <ids>", "comma-separated component ids (e.g. auth,docker,ci)").option("--js-pm <pm>", "JS package manager: pnpm | npm (default: auto-detect)").option("--py-pm <pm>", "Python package manager: uv | pip (default: auto-detect)").option("-y, --yes", "skip prompts; use flags/defaults").option("-f, --force", "write into a non-empty directory").option("--install", "install dependencies after scaffolding (default: ask, off with --yes)").action(async (name, opts) => {
181
+ program.command("create", { isDefault: true }).argument("[name]", "project name").description("Scaffold a new project").option("-d, --dir <path>", "target directory").option("--server", "include the Django server").option("--no-server", "exclude the server").option("--web", "include the Next.js web app").option("--no-web", "exclude the web app").option("--mobile", "include the Expo mobile app").option("--no-mobile", "exclude the mobile app").option("--with <ids>", "comma-separated component ids (e.g. auth,docker,ci)").option("--js-pm <pm>", "JS package manager: pnpm | npm (default: auto-detect)").option("--py-pm <pm>", "Python package manager: uv | pip (default: auto-detect)").option("-y, --yes", "skip prompts; use flags/defaults").option("-f, --force", "write into a non-empty directory").option("--install", "install dependencies after scaffolding (default: ask, off with --yes)").option("--git", "initialize a git repo + initial commit (default: ask, off with --yes)").option("--no-git", "skip git initialization").action(async (name, opts) => {
182
182
  const flags = { ...opts, name: name ?? opts.name };
183
183
  await runCreate(flags);
184
184
  });
@@ -16,7 +16,7 @@
16
16
  }
17
17
  },
18
18
  "notes": [
19
- "PostgreSQL: set DATABASE_URL in .env (already added to .env.example).",
19
+ "PostgreSQL: DATABASE_URL lives in apps/server/.env (created for you, with a matching .env.example).",
20
20
  "Need a local database? Include the 'docker' component and run `make db-up`."
21
21
  ]
22
22
  }
@@ -14,6 +14,6 @@
14
14
  },
15
15
  "notes": [
16
16
  "Local DB: `make db-up` (docker compose). Server image: `docker build apps/server`.",
17
- "Postgres creds/port come from .env (POSTGRES_USER/PASSWORD/DB/PORT); keep them in sync with DATABASE_URL."
17
+ "Postgres creds/port come from the root `.env` (POSTGRES_USER/PASSWORD/DB/PORT); keep them in sync with DATABASE_URL in apps/server/.env."
18
18
  ]
19
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "partweave",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "A modular full-stack scaffolder — pick the parts you need (Django server, Next.js web, Expo mobile) and generate only that code, wired together.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",