partweave 0.3.1 → 0.3.3
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,100 @@
|
|
|
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
|
|
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 GLYPHS = {
|
|
13
|
+
p: ["\u2588\u2588\u2588 ", "\u2588 \u2588", "\u2588\u2588\u2588 ", "\u2588 ", "\u2588 "],
|
|
14
|
+
a: [" \u2588\u2588 ", "\u2588 \u2588", "\u2588\u2588\u2588\u2588", "\u2588 \u2588", "\u2588 \u2588"],
|
|
15
|
+
r: ["\u2588\u2588\u2588 ", "\u2588 \u2588", "\u2588\u2588\u2588 ", "\u2588 \u2588 ", "\u2588 \u2588"],
|
|
16
|
+
t: ["\u2588\u2588\u2588\u2588\u2588", " \u2588 ", " \u2588 ", " \u2588 ", " \u2588 "],
|
|
17
|
+
w: ["\u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588 \u2588", "\u2588\u2588 \u2588\u2588", "\u2588 \u2588"],
|
|
18
|
+
e: ["\u2588\u2588\u2588\u2588", "\u2588 ", "\u2588\u2588\u2588 ", "\u2588 ", "\u2588\u2588\u2588\u2588"],
|
|
19
|
+
v: ["\u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588", " \u2588 \u2588 ", " \u2588 "]
|
|
20
|
+
};
|
|
21
|
+
var weaveCell = (row, col) => (row + col) % 2 ? "\u2593" : "\u2588";
|
|
22
|
+
var LOOM = ["\u2503\u2501\u2503\u2501", "\u2501\u2503\u2501\u2503", "\u2503\u2501\u2503\u2501", "\u2501\u2503\u2501\u2503", "\u2503\u2501\u2503\u2501"];
|
|
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 paintFace(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 paintShadow(ch, mode) {
|
|
58
|
+
if (ch === " ") return " ";
|
|
59
|
+
if (mode === "none") return ch;
|
|
60
|
+
if (mode === "basic") return pc.gray(ch);
|
|
61
|
+
return `\x1B[38;2;110;110;110m${ch}\x1B[39m`;
|
|
62
|
+
}
|
|
63
|
+
function paintThread(ch, mode) {
|
|
64
|
+
if (ch === " ") return " ";
|
|
65
|
+
if (mode === "none") return ch;
|
|
66
|
+
if (mode === "basic") return pc.dim(ch);
|
|
67
|
+
return `\x1B[38;2;120;110;140m${ch}\x1B[39m`;
|
|
68
|
+
}
|
|
69
|
+
function word2tone(word, mode) {
|
|
70
|
+
if (mode === "none") return word;
|
|
71
|
+
return [...word].map((ch, i) => paintFace(ch, word.length > 1 ? i / (word.length - 1) : 0, mode)).join("");
|
|
72
|
+
}
|
|
73
|
+
function compact(mode) {
|
|
74
|
+
return " " + (mode === "none" ? "partweave" : word2tone("partweave", mode));
|
|
75
|
+
}
|
|
76
|
+
function renderBanner() {
|
|
77
|
+
const mode = colorMode();
|
|
78
|
+
if (!process.stdout.isTTY) return compact(mode);
|
|
79
|
+
const rows = [];
|
|
80
|
+
for (let r = 0; r < HEIGHT; r++) {
|
|
81
|
+
rows.push([...WORD].map((ch) => GLYPHS[ch]?.[r] ?? "").join(" "));
|
|
82
|
+
}
|
|
83
|
+
const glyphW = rows[0].length;
|
|
84
|
+
const total = 2 + LOOM[0].length + 1 + (glyphW + 1);
|
|
85
|
+
if (total > (process.stdout.columns ?? 80)) return compact(mode);
|
|
86
|
+
const front = (r, c) => r >= 0 && r < HEIGHT && c >= 0 && c < glyphW && rows[r][c] === "\u2588";
|
|
87
|
+
const out = [];
|
|
88
|
+
for (let R = 0; R < HEIGHT; R++) {
|
|
89
|
+
const loom = [...LOOM[R]].map((ch) => paintThread(ch, mode)).join("");
|
|
90
|
+
let mark = "";
|
|
91
|
+
for (let C = 0; C <= glyphW; C++) {
|
|
92
|
+
const t = glyphW > 1 ? Math.min(C, glyphW - 1) / (glyphW - 1) : 0;
|
|
93
|
+
if (front(R, C)) mark += paintFace(weaveCell(R, C), t, mode);
|
|
94
|
+
else if (front(R, C - 1)) mark += paintShadow("\u2591", mode);
|
|
95
|
+
else mark += " ";
|
|
96
|
+
}
|
|
97
|
+
out.push(" " + loom + " " + mark);
|
|
98
|
+
}
|
|
99
|
+
return out.join("\n");
|
|
100
|
+
}
|
|
9
101
|
|
|
10
102
|
// src/compose.ts
|
|
11
103
|
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
@@ -26,14 +118,19 @@ function injectAtAnchor(content, anchorId, lines) {
|
|
|
26
118
|
throw new Error(`Anchor <partweave:${anchorId}> not found in target file`);
|
|
27
119
|
}
|
|
28
120
|
const indent = match[1] ?? "";
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
);
|
|
32
|
-
|
|
121
|
+
const before = content.slice(0, match.index).split("\n");
|
|
122
|
+
if (before[before.length - 1] === "") before.pop();
|
|
123
|
+
const block = /* @__PURE__ */ new Set();
|
|
124
|
+
for (let i = before.length - 1; i >= 0; i--) {
|
|
125
|
+
const trimmed = before[i].trim();
|
|
126
|
+
if (trimmed === "") break;
|
|
127
|
+
block.add(trimmed);
|
|
128
|
+
}
|
|
129
|
+
const toInsert = lines.filter((l) => !block.has(l.trim()));
|
|
33
130
|
if (toInsert.length === 0) return { content, inserted: 0 };
|
|
34
|
-
const
|
|
131
|
+
const insertion = toInsert.map((l) => l ? indent + l : "").join("\n") + "\n";
|
|
35
132
|
const anchorStart = match.index;
|
|
36
|
-
const newContent = content.slice(0, anchorStart) +
|
|
133
|
+
const newContent = content.slice(0, anchorStart) + insertion + content.slice(anchorStart);
|
|
37
134
|
return { content: newContent, inserted: toInsert.length };
|
|
38
135
|
}
|
|
39
136
|
function parseDep(dep) {
|
|
@@ -50,34 +147,35 @@ function normalizeWorkspaceDeps(deps, workspaceRange) {
|
|
|
50
147
|
return dep;
|
|
51
148
|
});
|
|
52
149
|
}
|
|
150
|
+
function higherVersion(a, b) {
|
|
151
|
+
const core = (v) => {
|
|
152
|
+
const m = v.match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
153
|
+
if (!m || m[1] === void 0) return null;
|
|
154
|
+
return [m[1], m[2], m[3]].map((n) => n === void 0 ? 0 : Number(n));
|
|
155
|
+
};
|
|
156
|
+
const ca = core(a);
|
|
157
|
+
const cb = core(b);
|
|
158
|
+
if (!ca || !cb) return a;
|
|
159
|
+
for (let i = 0; i < 3; i++) {
|
|
160
|
+
if (ca[i] !== cb[i]) return ca[i] > cb[i] ? a : b;
|
|
161
|
+
}
|
|
162
|
+
return a;
|
|
163
|
+
}
|
|
53
164
|
function mergePackageJsonDeps(pkgJson, deps, field = "dependencies") {
|
|
54
165
|
if (deps.length === 0) return pkgJson;
|
|
55
166
|
const pkg = JSON.parse(pkgJson);
|
|
56
167
|
const bucket = pkg[field] ?? {};
|
|
57
168
|
for (const dep of deps) {
|
|
58
169
|
const { name, version } = parseDep(dep);
|
|
59
|
-
|
|
170
|
+
bucket[name] = name in bucket ? higherVersion(bucket[name], version) : version;
|
|
60
171
|
}
|
|
61
172
|
pkg[field] = Object.fromEntries(
|
|
62
173
|
Object.entries(bucket).sort(([a], [b]) => a.localeCompare(b))
|
|
63
174
|
);
|
|
64
175
|
return JSON.stringify(pkg, null, 2) + "\n";
|
|
65
176
|
}
|
|
66
|
-
function
|
|
67
|
-
|
|
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;
|
|
177
|
+
function pyDepName(dep) {
|
|
178
|
+
return dep.trim().replace(/^["']|["']$/g, "").split(/[<>=!~;[\s]/)[0].trim().toLowerCase();
|
|
81
179
|
}
|
|
82
180
|
|
|
83
181
|
// src/fsutil.ts
|
|
@@ -548,45 +646,82 @@ function buildTsconfigBase(ctx) {
|
|
|
548
646
|
2
|
|
549
647
|
) + "\n";
|
|
550
648
|
}
|
|
551
|
-
function
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
649
|
+
function envScopeFor(key) {
|
|
650
|
+
if (key.startsWith("POSTGRES_")) return "root";
|
|
651
|
+
if (key.startsWith("NEXT_PUBLIC_")) return "web";
|
|
652
|
+
if (key.startsWith("EXPO_PUBLIC_")) return "mobile";
|
|
653
|
+
return "server";
|
|
654
|
+
}
|
|
655
|
+
function buildEnvFiles(ctx, modules) {
|
|
656
|
+
const componentBlocks = { server: [], web: [], mobile: [], root: [] };
|
|
657
|
+
for (const m of modules) {
|
|
658
|
+
const byScope = { server: [], web: [], mobile: [], root: [] };
|
|
659
|
+
for (const [key, value] of Object.entries(m.manifest.env)) {
|
|
660
|
+
byScope[envScopeFor(key)].push(`${key}=${value}`);
|
|
661
|
+
}
|
|
662
|
+
for (const scope of ["server", "web", "mobile", "root"]) {
|
|
663
|
+
if (byScope[scope].length === 0) continue;
|
|
664
|
+
componentBlocks[scope].push(`# ${m.manifest.title}`, ...byScope[scope], "");
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
const render2 = (lines) => lines.join("\n").replace(/\n*$/, "\n");
|
|
668
|
+
const files = [];
|
|
560
669
|
if (ctx.hasServer) {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
"#
|
|
564
|
-
"
|
|
565
|
-
|
|
670
|
+
const serverBase = (secretLine) => [
|
|
671
|
+
`# ${ctx.projectName} \u2014 server (Django). Read by apps/server.`,
|
|
672
|
+
"# `.env` is gitignored; keep real secrets here, not in `.env.example`.",
|
|
673
|
+
"",
|
|
674
|
+
"# Unique per-project key; signs sessions and JWTs.",
|
|
675
|
+
secretLine,
|
|
566
676
|
"DJANGO_DEBUG=true",
|
|
567
|
-
"DJANGO_ALLOWED_HOSTS
|
|
568
|
-
"#
|
|
569
|
-
"#
|
|
677
|
+
"# Leave DJANGO_ALLOWED_HOSTS unset in dev: with DEBUG on, any host is allowed,",
|
|
678
|
+
"# so a phone/simulator can reach the server over your LAN (e.g. 192.168.x.y).",
|
|
679
|
+
"# Set it (comma-separated) in production, e.g. DJANGO_ALLOWED_HOSTS=api.example.com",
|
|
680
|
+
"# Origins allowed to call the API when DEBUG is off (comma-separated).",
|
|
570
681
|
"# DJANGO_CORS_ALLOWED_ORIGINS=https://app.example.com",
|
|
571
|
-
"# DATABASE_URL \u2014
|
|
572
|
-
"# to switch (the `db-postgres` component sets a Postgres DSN here for you).",
|
|
682
|
+
"# DATABASE_URL \u2014 unset uses local SQLite; the db-postgres component sets a Postgres DSN.",
|
|
573
683
|
""
|
|
574
|
-
|
|
684
|
+
];
|
|
685
|
+
files.push({
|
|
686
|
+
dir: "apps/server",
|
|
687
|
+
example: render2([...serverBase("DJANGO_SECRET_KEY=replace-with-a-generated-secret"), ...componentBlocks.server]),
|
|
688
|
+
env: render2([...serverBase(`DJANGO_SECRET_KEY=${randomBytes(48).toString("base64url")}`), ...componentBlocks.server])
|
|
689
|
+
});
|
|
575
690
|
}
|
|
576
691
|
if (ctx.hasWeb) {
|
|
577
|
-
|
|
692
|
+
const web = [
|
|
693
|
+
`# ${ctx.projectName} \u2014 web (Next.js). Read by apps/web.`,
|
|
694
|
+
"# Only NEXT_PUBLIC_* is exposed to the browser.",
|
|
695
|
+
"NEXT_PUBLIC_API_URL=http://localhost:8000",
|
|
696
|
+
"",
|
|
697
|
+
...componentBlocks.web
|
|
698
|
+
];
|
|
699
|
+
const body = render2(web);
|
|
700
|
+
files.push({ dir: "apps/web", example: body, env: body });
|
|
578
701
|
}
|
|
579
702
|
if (ctx.hasMobile) {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
"# In dev the app auto-detects your
|
|
583
|
-
"#
|
|
584
|
-
"# in the environment or apps/mobile/.env (Expo reads EXPO_PUBLIC_* from there).",
|
|
703
|
+
const mobile = [
|
|
704
|
+
`# ${ctx.projectName} \u2014 mobile (Expo). Read by apps/mobile.`,
|
|
705
|
+
"# Only EXPO_PUBLIC_* is exposed to the app. In dev the app auto-detects your",
|
|
706
|
+
"# machine's LAN IP, so this can stay unset; set it for simulators/device/production.",
|
|
585
707
|
"# EXPO_PUBLIC_API_URL=http://localhost:8000",
|
|
586
|
-
""
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
708
|
+
"",
|
|
709
|
+
...componentBlocks.mobile
|
|
710
|
+
];
|
|
711
|
+
const body = render2(mobile);
|
|
712
|
+
files.push({ dir: "apps/mobile", example: body, env: body });
|
|
713
|
+
}
|
|
714
|
+
if (componentBlocks.root.length > 0) {
|
|
715
|
+
const root = [
|
|
716
|
+
`# ${ctx.projectName} \u2014 infrastructure. Read by docker compose (--env-file .env).`,
|
|
717
|
+
"# Keep these in sync with DATABASE_URL in apps/server/.env.",
|
|
718
|
+
"",
|
|
719
|
+
...componentBlocks.root
|
|
720
|
+
];
|
|
721
|
+
const body = render2(root);
|
|
722
|
+
files.push({ dir: "", example: body, env: body });
|
|
723
|
+
}
|
|
724
|
+
return files;
|
|
590
725
|
}
|
|
591
726
|
function jsCiSteps(ctx) {
|
|
592
727
|
if (ctx.jsPm === "npm") {
|
|
@@ -711,7 +846,18 @@ function buildReadme(ctx, modules) {
|
|
|
711
846
|
if (ctx.hasWeb) parts.push("npm run web # http://localhost:3000");
|
|
712
847
|
if (ctx.hasMobile) parts.push("npm run mobile # Expo dev server");
|
|
713
848
|
parts.push("```", "");
|
|
714
|
-
parts.push("
|
|
849
|
+
parts.push("## Configuration", "");
|
|
850
|
+
parts.push(
|
|
851
|
+
"Each app reads its **own** env file, created for you (and gitignored) with a",
|
|
852
|
+
"committed `.env.example` template alongside it. Edit the `.env` to change values:",
|
|
853
|
+
""
|
|
854
|
+
);
|
|
855
|
+
const envFiles = [];
|
|
856
|
+
if (ctx.hasServer) envFiles.push("- `apps/server/.env` \u2014 Django secret key, allowed hosts, `DATABASE_URL`");
|
|
857
|
+
if (ctx.hasWeb) envFiles.push("- `apps/web/.env` \u2014 `NEXT_PUBLIC_*` (browser-exposed)");
|
|
858
|
+
if (ctx.hasMobile) envFiles.push("- `apps/mobile/.env` \u2014 `EXPO_PUBLIC_*` (app-exposed)");
|
|
859
|
+
if (hasDocker) envFiles.push("- `.env` (root) \u2014 `POSTGRES_*` for the database container");
|
|
860
|
+
parts.push(...envFiles, "");
|
|
715
861
|
return parts.join("\n");
|
|
716
862
|
}
|
|
717
863
|
function buildServerDockerfile(ctx) {
|
|
@@ -838,7 +984,7 @@ var ManifestSchema = z.object({
|
|
|
838
984
|
conflicts: z.array(z.string()).default([]),
|
|
839
985
|
/** capability/interface this module satisfies (for conflict grouping) */
|
|
840
986
|
provides: z.string().optional(),
|
|
841
|
-
/** env keys → default value
|
|
987
|
+
/** 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
988
|
env: z.record(z.string()).default({}),
|
|
843
989
|
/** per-target wiring (files are copied separately from wiring injection) */
|
|
844
990
|
wiring: z.record(z.enum(TARGETS), WiringForTargetSchema).default({}),
|
|
@@ -928,6 +1074,25 @@ function injectIntoFiles(index, anchorId, lines, targetLabel) {
|
|
|
928
1074
|
writeFileSync2(file, next);
|
|
929
1075
|
}
|
|
930
1076
|
}
|
|
1077
|
+
function injectPyDeps(index, deps, targetLabel) {
|
|
1078
|
+
const files = index.get("deps");
|
|
1079
|
+
if (!files || files.length === 0) {
|
|
1080
|
+
throw new Error(
|
|
1081
|
+
`Wiring error: anchor <partweave:deps> not found in ${targetLabel}. Add the anchor to the _core/${targetLabel} scaffold.`
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
for (const file of files) {
|
|
1085
|
+
const content = readFileSync2(file, "utf8");
|
|
1086
|
+
const region = content.match(/dependencies\s*=\s*\[([\s\S]*?)\]/)?.[1] ?? content;
|
|
1087
|
+
const present = new Set(
|
|
1088
|
+
[...region.matchAll(/["']([^"']+)["']/g)].map((m) => pyDepName(m[1]))
|
|
1089
|
+
);
|
|
1090
|
+
const fresh = deps.filter((d) => !present.has(pyDepName(d)));
|
|
1091
|
+
if (fresh.length === 0) continue;
|
|
1092
|
+
const { content: next } = injectAtAnchor(content, "deps", fresh.map((d) => `"${d}",`));
|
|
1093
|
+
writeFileSync2(file, next);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
931
1096
|
function applyWiring(outDir, targets, modules, workspaceRange) {
|
|
932
1097
|
const indexes = /* @__PURE__ */ new Map();
|
|
933
1098
|
for (const t of targets) {
|
|
@@ -945,8 +1110,7 @@ function applyWiring(outDir, targets, modules, workspaceRange) {
|
|
|
945
1110
|
}
|
|
946
1111
|
if (wiring.deps?.length) {
|
|
947
1112
|
if (t === "server") {
|
|
948
|
-
|
|
949
|
-
injectIntoFiles(index, "deps", lines, t);
|
|
1113
|
+
injectPyDeps(index, wiring.deps, t);
|
|
950
1114
|
} else {
|
|
951
1115
|
const pkgPath = join2(targetDir, "package.json");
|
|
952
1116
|
if (existsSync2(pkgPath)) {
|
|
@@ -961,14 +1125,19 @@ function applyWiring(outDir, targets, modules, workspaceRange) {
|
|
|
961
1125
|
}
|
|
962
1126
|
}
|
|
963
1127
|
}
|
|
964
|
-
function
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1128
|
+
function writeEnvFiles(outDir, ctx, modules) {
|
|
1129
|
+
const written = [];
|
|
1130
|
+
for (const file of buildEnvFiles(ctx, modules)) {
|
|
1131
|
+
const exampleRel = join2(file.dir, ".env.example");
|
|
1132
|
+
writeFileEnsured(join2(outDir, exampleRel), file.example);
|
|
1133
|
+
written.push(exampleRel);
|
|
1134
|
+
const envRel = join2(file.dir, ".env");
|
|
1135
|
+
if (!existsSync2(join2(outDir, envRel))) {
|
|
1136
|
+
writeFileEnsured(join2(outDir, envRel), file.env);
|
|
1137
|
+
written.push(envRel);
|
|
1138
|
+
}
|
|
970
1139
|
}
|
|
971
|
-
|
|
1140
|
+
return written;
|
|
972
1141
|
}
|
|
973
1142
|
function writeStructuralRootFiles(outDir, ctx, modules) {
|
|
974
1143
|
const workspace = buildJsWorkspace(ctx);
|
|
@@ -984,7 +1153,6 @@ function writeStructuralRootFiles(outDir, ctx, modules) {
|
|
|
984
1153
|
if (pipSync) writeFileEnsured(join2(outDir, "apps/server/scripts/sync_deps.py"), pipSync);
|
|
985
1154
|
writeFileEnsured(join2(outDir, "scripts/run.mjs"), buildTaskRunner(ctx, hasDocker));
|
|
986
1155
|
writeFileEnsured(join2(outDir, "Makefile"), buildMakefile(ctx, hasDocker));
|
|
987
|
-
writeFileEnsured(join2(outDir, ".env.example"), buildBaseEnv(ctx));
|
|
988
1156
|
}
|
|
989
1157
|
function compose(opts) {
|
|
990
1158
|
const { selection, registry, scaffoldTargets, wireTargets, rootFiles } = opts;
|
|
@@ -1013,7 +1181,7 @@ function compose(opts) {
|
|
|
1013
1181
|
}
|
|
1014
1182
|
}
|
|
1015
1183
|
applyWiring(outDir, wireTargets, modules, jsPmProfile(ctx.jsPm).workspaceRange);
|
|
1016
|
-
|
|
1184
|
+
if (rootFiles !== "none") written.push(...writeEnvFiles(outDir, ctx, modules));
|
|
1017
1185
|
if (selection.modules.includes("ci")) {
|
|
1018
1186
|
for (const [rel, content] of Object.entries(buildCiWorkflows(ctx))) {
|
|
1019
1187
|
writeFileEnsured(join2(outDir, rel), content);
|
|
@@ -1209,11 +1377,28 @@ function resolveModules(registry, chosen) {
|
|
|
1209
1377
|
provided.set(cap, id);
|
|
1210
1378
|
}
|
|
1211
1379
|
}
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1380
|
+
const ids = [...resolved];
|
|
1381
|
+
const deps = new Map(
|
|
1382
|
+
ids.map((id) => [id, registry.require(id).manifest.requires.filter((r) => resolved.has(r))])
|
|
1383
|
+
);
|
|
1384
|
+
const indegree = new Map(ids.map((id) => [id, deps.get(id).length]));
|
|
1385
|
+
const dependents = new Map(ids.map((id) => [id, []]));
|
|
1386
|
+
for (const id of ids) for (const d of deps.get(id)) dependents.get(d).push(id);
|
|
1387
|
+
const byId = (a, b) => a.localeCompare(b);
|
|
1388
|
+
const ready = ids.filter((id) => indegree.get(id) === 0).sort(byId);
|
|
1389
|
+
const order = [];
|
|
1390
|
+
while (ready.length > 0) {
|
|
1391
|
+
const id = ready.shift();
|
|
1392
|
+
order.push(id);
|
|
1393
|
+
for (const dep of dependents.get(id)) {
|
|
1394
|
+
const n = indegree.get(dep) - 1;
|
|
1395
|
+
indegree.set(dep, n);
|
|
1396
|
+
if (n === 0) {
|
|
1397
|
+
ready.push(dep);
|
|
1398
|
+
ready.sort(byId);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1217
1402
|
return { modules: order, autoAdded: [...autoAdded].sort() };
|
|
1218
1403
|
}
|
|
1219
1404
|
function validateApps(registry, moduleIds, apps) {
|
|
@@ -1242,12 +1427,13 @@ import {
|
|
|
1242
1427
|
confirm as confirm2,
|
|
1243
1428
|
isCancel as isCancel2,
|
|
1244
1429
|
multiselect,
|
|
1430
|
+
note,
|
|
1245
1431
|
select,
|
|
1246
1432
|
text
|
|
1247
1433
|
} from "@clack/prompts";
|
|
1248
|
-
import
|
|
1434
|
+
import pc2 from "picocolors";
|
|
1249
1435
|
import { resolve as resolve2 } from "path";
|
|
1250
|
-
var MULTI_HINT =
|
|
1436
|
+
var MULTI_HINT = pc2.dim("\u2191/\u2193 move \xB7 space toggle \xB7 a all \xB7 enter confirm");
|
|
1251
1437
|
function bail(v) {
|
|
1252
1438
|
if (isCancel2(v)) {
|
|
1253
1439
|
cancel("Cancelled.");
|
|
@@ -1327,9 +1513,19 @@ async function promptCreate(registry, defaults) {
|
|
|
1327
1513
|
bail(pyRaw);
|
|
1328
1514
|
pyPm = pyRaw;
|
|
1329
1515
|
}
|
|
1330
|
-
const
|
|
1331
|
-
|
|
1332
|
-
|
|
1516
|
+
const row = (k, v) => `${pc2.dim(k.padEnd(8))} ${v}`;
|
|
1517
|
+
const tooling = [anyJs ? jsPm : null, apps.includes("server") ? pyPm : null].filter(Boolean).join(pc2.dim(" \xB7 "));
|
|
1518
|
+
note(
|
|
1519
|
+
[
|
|
1520
|
+
row("project", pc2.bold(projectName)),
|
|
1521
|
+
row("where", outDir),
|
|
1522
|
+
row("apps", apps.join(pc2.dim(" \xB7 "))),
|
|
1523
|
+
row("add-ons", modules.length ? modules.join(pc2.dim(" \xB7 ")) : pc2.dim("none")),
|
|
1524
|
+
row("tooling", tooling)
|
|
1525
|
+
].join("\n"),
|
|
1526
|
+
"Review"
|
|
1527
|
+
);
|
|
1528
|
+
const ok = await confirm2({ message: "Scaffold this?" });
|
|
1333
1529
|
bail(ok);
|
|
1334
1530
|
if (!ok) {
|
|
1335
1531
|
cancel("Cancelled.");
|
|
@@ -1339,6 +1535,21 @@ async function promptCreate(registry, defaults) {
|
|
|
1339
1535
|
}
|
|
1340
1536
|
|
|
1341
1537
|
// src/commands/create.ts
|
|
1538
|
+
function isInsideGitRepo(dir) {
|
|
1539
|
+
return spawnSync3("git", ["rev-parse", "--is-inside-work-tree"], { cwd: dir, stdio: "ignore" }).status === 0;
|
|
1540
|
+
}
|
|
1541
|
+
function initGit(dir) {
|
|
1542
|
+
if (spawnSync3("git", ["init", "-b", "main"], { cwd: dir, stdio: "ignore" }).status !== 0) {
|
|
1543
|
+
spawnSync3("git", ["init"], { cwd: dir, stdio: "ignore" });
|
|
1544
|
+
spawnSync3("git", ["branch", "-M", "main"], { cwd: dir, stdio: "ignore" });
|
|
1545
|
+
}
|
|
1546
|
+
spawnSync3("git", ["add", "-A"], { cwd: dir, stdio: "ignore" });
|
|
1547
|
+
const msg = "Initial commit (scaffolded with partweave)";
|
|
1548
|
+
const opts = { cwd: dir, stdio: "ignore" };
|
|
1549
|
+
if (spawnSync3("git", ["commit", "-m", msg], opts).status === 0) return true;
|
|
1550
|
+
const id = ["-c", "user.name=partweave", "-c", "user.email=partweave@users.noreply.github.com"];
|
|
1551
|
+
return spawnSync3("git", [...id, "commit", "-m", msg], opts).status === 0;
|
|
1552
|
+
}
|
|
1342
1553
|
function resolvePm(value, allowed, detect, flag) {
|
|
1343
1554
|
if (value === void 0) return detect();
|
|
1344
1555
|
if (allowed.includes(value)) return value;
|
|
@@ -1360,7 +1571,8 @@ function defaultModules(registry, apps) {
|
|
|
1360
1571
|
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
1572
|
}
|
|
1362
1573
|
async function runCreate(flags) {
|
|
1363
|
-
|
|
1574
|
+
console.log("\n" + renderBanner());
|
|
1575
|
+
intro(pc3.dim("full-stack scaffolder \u2014 pick the parts, own the code"));
|
|
1364
1576
|
const registry = new Registry();
|
|
1365
1577
|
const flagApps = appsFromFlags(flags);
|
|
1366
1578
|
const nonInteractive = flags.yes === true || flagApps !== null;
|
|
@@ -1461,6 +1673,27 @@ async function runCreate(flags) {
|
|
|
1461
1673
|
installed = false;
|
|
1462
1674
|
}
|
|
1463
1675
|
}
|
|
1676
|
+
let gitInitialized = false;
|
|
1677
|
+
const gitAvailable = hasCommand("git");
|
|
1678
|
+
const alreadyRepo = gitAvailable && isInsideGitRepo(choices.outDir);
|
|
1679
|
+
let wantGit;
|
|
1680
|
+
if (flags.git !== void 0) {
|
|
1681
|
+
wantGit = flags.git;
|
|
1682
|
+
} else if (nonInteractive) {
|
|
1683
|
+
wantGit = false;
|
|
1684
|
+
} else {
|
|
1685
|
+
const ans = await confirm3({ message: "Initialize a git repository?" });
|
|
1686
|
+
wantGit = !isCancel3(ans) && ans === true;
|
|
1687
|
+
}
|
|
1688
|
+
if (wantGit && gitAvailable && !alreadyRepo) {
|
|
1689
|
+
gitInitialized = initGit(choices.outDir);
|
|
1690
|
+
if (gitInitialized) log2.success("Initialized a git repository (branch main, initial commit).");
|
|
1691
|
+
else log2.warn("Couldn't create the initial git commit \u2014 the repo was left uninitialized.");
|
|
1692
|
+
} else if (wantGit && !gitAvailable) {
|
|
1693
|
+
log2.warn("git isn't installed \u2014 skipped repository initialization.");
|
|
1694
|
+
} else if (wantGit && alreadyRepo) {
|
|
1695
|
+
log2.info("Target is already inside a git repository \u2014 skipped `git init`.");
|
|
1696
|
+
}
|
|
1464
1697
|
const rel = basename(choices.outDir);
|
|
1465
1698
|
const hasDocker = resolved.modules.includes("docker");
|
|
1466
1699
|
const steps = [`cd ${rel}`];
|
|
@@ -1471,12 +1704,12 @@ async function runCreate(flags) {
|
|
|
1471
1704
|
}
|
|
1472
1705
|
if (choices.apps.includes("web")) steps.push("npm run web");
|
|
1473
1706
|
if (choices.apps.includes("mobile")) steps.push("npm run mobile");
|
|
1474
|
-
|
|
1475
|
-
steps.join("\n") + "\n\n" +
|
|
1707
|
+
note2(
|
|
1708
|
+
steps.join("\n") + "\n\n" + pc3.dim("These work on macOS, Linux & Windows. On macOS/Linux, `make <task>` works too."),
|
|
1476
1709
|
"Next steps"
|
|
1477
1710
|
);
|
|
1478
|
-
if (result.notes.length)
|
|
1479
|
-
outro(
|
|
1711
|
+
if (result.notes.length) note2(result.notes.join("\n"), "Notes");
|
|
1712
|
+
outro(pc3.green("Done."));
|
|
1480
1713
|
}
|
|
1481
1714
|
|
|
1482
1715
|
export {
|
|
@@ -1496,4 +1729,4 @@ export {
|
|
|
1496
1729
|
validateApps,
|
|
1497
1730
|
runCreate
|
|
1498
1731
|
};
|
|
1499
|
-
//# sourceMappingURL=chunk-
|
|
1732
|
+
//# sourceMappingURL=chunk-I2IJROBO.js.map
|
package/dist/create-bin.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
selectedTargets,
|
|
16
16
|
validateApps,
|
|
17
17
|
writeProjectManifest
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-I2IJROBO.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:
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.3.3",
|
|
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",
|