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
|
|
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
|
|
30
|
-
|
|
31
|
-
);
|
|
32
|
-
|
|
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
|
|
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) +
|
|
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
|
-
|
|
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
|
|
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;
|
|
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
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
"#
|
|
564
|
-
"
|
|
565
|
-
|
|
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
|
|
568
|
-
"#
|
|
569
|
-
"#
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
"# In dev the app auto-detects your
|
|
583
|
-
"#
|
|
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
|
-
|
|
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("
|
|
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
|
|
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
|
-
|
|
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
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
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
|
|
1414
|
+
import pc2 from "picocolors";
|
|
1249
1415
|
import { resolve as resolve2 } from "path";
|
|
1250
|
-
var MULTI_HINT =
|
|
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
|
|
1331
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1475
|
-
steps.join("\n") + "\n\n" +
|
|
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)
|
|
1479
|
-
outro(
|
|
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-
|
|
1712
|
+
//# sourceMappingURL=chunk-Y5FZVVV6.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-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:
|
|
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.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",
|