partweave 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/dist/chunk-2IGBGILB.js +1230 -0
  4. package/dist/chunk-2IGBGILB.js.map +1 -0
  5. package/dist/create-bin.js +17 -0
  6. package/dist/create-bin.js.map +1 -0
  7. package/dist/index.js +164 -0
  8. package/dist/index.js.map +1 -0
  9. package/modules/_core/api-client/package.json +22 -0
  10. package/modules/_core/api-client/src/index.ts +32 -0
  11. package/modules/_core/api-client/src/schema.d.ts +12 -0
  12. package/modules/_core/api-client/tsconfig.json +7 -0
  13. package/modules/_core/mobile/app/_layout.tsx +10 -0
  14. package/modules/_core/mobile/app/index.tsx +24 -0
  15. package/modules/_core/mobile/app.json +18 -0
  16. package/modules/_core/mobile/babel.config.js +6 -0
  17. package/modules/_core/mobile/expo-env.d.ts +3 -0
  18. package/modules/_core/mobile/jest.config.js +10 -0
  19. package/modules/_core/mobile/metro.config.js +18 -0
  20. package/modules/_core/mobile/package.json +36 -0
  21. package/modules/_core/mobile/src/nav.ts +9 -0
  22. package/modules/_core/mobile/src/providers.tsx +17 -0
  23. package/modules/_core/mobile/tsconfig.json +10 -0
  24. package/modules/_core/root/.editorconfig +15 -0
  25. package/modules/_core/root/_gitignore +29 -0
  26. package/modules/_core/server/.python-version +1 -0
  27. package/modules/_core/server/config/__init__.py +0 -0
  28. package/modules/_core/server/config/asgi.py +7 -0
  29. package/modules/_core/server/config/settings.py +127 -0
  30. package/modules/_core/server/config/urls.py +17 -0
  31. package/modules/_core/server/config/wsgi.py +7 -0
  32. package/modules/_core/server/manage.py +21 -0
  33. package/modules/_core/server/pyproject.toml +38 -0
  34. package/modules/_core/server/tests/test_health.py +4 -0
  35. package/modules/_core/shared/package.json +17 -0
  36. package/modules/_core/shared/src/index.ts +11 -0
  37. package/modules/_core/shared/tsconfig.json +7 -0
  38. package/modules/_core/web/app/globals.css +3 -0
  39. package/modules/_core/web/app/layout.tsx +19 -0
  40. package/modules/_core/web/app/page.tsx +21 -0
  41. package/modules/_core/web/app/providers.tsx +19 -0
  42. package/modules/_core/web/next-env.d.ts +5 -0
  43. package/modules/_core/web/next.config.mjs +9 -0
  44. package/modules/_core/web/package.json +31 -0
  45. package/modules/_core/web/postcss.config.mjs +6 -0
  46. package/modules/_core/web/src/nav.ts +9 -0
  47. package/modules/_core/web/tailwind.config.ts +9 -0
  48. package/modules/_core/web/tsconfig.json +18 -0
  49. package/modules/_core/web/vitest.config.mts +19 -0
  50. package/modules/auth/mobile/app/login.tsx +63 -0
  51. package/modules/auth/mobile/src/auth/auth-context.tsx +61 -0
  52. package/modules/auth/mobile/src/auth/client.test.ts +69 -0
  53. package/modules/auth/mobile/src/auth/client.ts +59 -0
  54. package/modules/auth/mobile/src/lib/config.ts +22 -0
  55. package/modules/auth/mobile/src/lib/token-store.ts +20 -0
  56. package/modules/auth/module.json +47 -0
  57. package/modules/auth/server/accounts/__init__.py +0 -0
  58. package/modules/auth/server/accounts/admin.py +4 -0
  59. package/modules/auth/server/accounts/apps.py +6 -0
  60. package/modules/auth/server/accounts/migrations/0001_initial.py +38 -0
  61. package/modules/auth/server/accounts/migrations/__init__.py +0 -0
  62. package/modules/auth/server/accounts/models.py +39 -0
  63. package/modules/auth/server/accounts/serializers.py +21 -0
  64. package/modules/auth/server/accounts/urls.py +11 -0
  65. package/modules/auth/server/accounts/views.py +26 -0
  66. package/modules/auth/server/tests/test_auth.py +44 -0
  67. package/modules/auth/shared/src/auth.ts +27 -0
  68. package/modules/auth/web/app/login/page.tsx +61 -0
  69. package/modules/auth/web/src/auth/auth-context.test.tsx +66 -0
  70. package/modules/auth/web/src/auth/auth-context.tsx +63 -0
  71. package/modules/auth/web/src/auth/client.test.ts +73 -0
  72. package/modules/auth/web/src/auth/client.ts +59 -0
  73. package/modules/auth/web/src/lib/config.ts +2 -0
  74. package/modules/auth/web/src/lib/token-store.ts +23 -0
  75. package/modules/ci/module.json +11 -0
  76. package/modules/db-postgres/module.json +22 -0
  77. package/modules/docker/module.json +19 -0
  78. package/modules/docker/root/infra/docker-compose.yml +20 -0
  79. package/modules/docker/server/.dockerignore +7 -0
  80. package/modules/example/mobile/app/profile.test.tsx +44 -0
  81. package/modules/example/mobile/app/profile.tsx +39 -0
  82. package/modules/example/module.json +21 -0
  83. package/modules/example/web/app/profile/page.test.tsx +41 -0
  84. package/modules/example/web/app/profile/page.tsx +34 -0
  85. package/modules/storage/module.json +34 -0
  86. package/modules/storage/server/storage/__init__.py +0 -0
  87. package/modules/storage/server/storage/base.py +24 -0
  88. package/modules/storage/server/storage/factory.py +20 -0
  89. package/modules/storage/server/storage/local.py +28 -0
  90. package/modules/storage/server/storage/s3.py +30 -0
  91. package/modules/storage/server/tests/test_storage.py +20 -0
  92. package/package.json +69 -0
@@ -0,0 +1,1230 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/create.ts
4
+ import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
5
+ import { basename, resolve as resolve3 } from "path";
6
+ import { intro, log, note, outro, spinner } from "@clack/prompts";
7
+ import pc2 from "picocolors";
8
+
9
+ // src/compose.ts
10
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
11
+ import { join as join2 } from "path";
12
+
13
+ // src/inject.ts
14
+ function anchorRegex(anchorId) {
15
+ return new RegExp(`^([ \\t]*).*<partweave:${escapeRe(anchorId)}>.*$`, "m");
16
+ }
17
+ function escapeRe(s) {
18
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19
+ }
20
+ function injectAtAnchor(content, anchorId, lines) {
21
+ if (lines.length === 0) return { content, inserted: 0 };
22
+ const re = anchorRegex(anchorId);
23
+ const match = re.exec(content);
24
+ if (!match) {
25
+ throw new Error(`Anchor <partweave:${anchorId}> not found in target file`);
26
+ }
27
+ const indent = match[1] ?? "";
28
+ const existing = new Set(
29
+ content.split("\n").map((l) => l.trim()).filter(Boolean)
30
+ );
31
+ const toInsert = lines.filter((l) => !existing.has(l.trim()));
32
+ if (toInsert.length === 0) return { content, inserted: 0 };
33
+ const block = toInsert.map((l) => l ? indent + l : "").join("\n") + "\n";
34
+ const anchorStart = match.index;
35
+ const newContent = content.slice(0, anchorStart) + block + content.slice(anchorStart);
36
+ return { content: newContent, inserted: toInsert.length };
37
+ }
38
+ function parseDep(dep) {
39
+ const at = dep.lastIndexOf("@");
40
+ if (at > 0) {
41
+ return { name: dep.slice(0, at), version: dep.slice(at + 1) };
42
+ }
43
+ return { name: dep, version: "latest" };
44
+ }
45
+ function mergePackageJsonDeps(pkgJson, deps, field = "dependencies") {
46
+ if (deps.length === 0) return pkgJson;
47
+ const pkg = JSON.parse(pkgJson);
48
+ const bucket = pkg[field] ?? {};
49
+ for (const dep of deps) {
50
+ const { name, version } = parseDep(dep);
51
+ if (!(name in bucket)) bucket[name] = version;
52
+ }
53
+ pkg[field] = Object.fromEntries(
54
+ Object.entries(bucket).sort(([a], [b]) => a.localeCompare(b))
55
+ );
56
+ return JSON.stringify(pkg, null, 2) + "\n";
57
+ }
58
+ function appendEnv(envBody, entries, heading) {
59
+ const keys = Object.keys(entries);
60
+ if (keys.length === 0) return envBody;
61
+ const present = new Set(
62
+ envBody.split("\n").map((l) => l.split("=")[0]?.trim()).filter(Boolean)
63
+ );
64
+ const missing = keys.filter((k) => !present.has(k));
65
+ if (missing.length === 0) return envBody;
66
+ let out = envBody.replace(/\n*$/, "\n");
67
+ if (heading) out += `
68
+ # ${heading}
69
+ `;
70
+ for (const k of missing) out += `${k}=${entries[k]}
71
+ `;
72
+ return out;
73
+ }
74
+
75
+ // src/fsutil.ts
76
+ import {
77
+ cpSync,
78
+ existsSync,
79
+ mkdirSync,
80
+ readdirSync,
81
+ readFileSync,
82
+ statSync,
83
+ writeFileSync
84
+ } from "fs";
85
+ import { dirname, join, relative } from "path";
86
+
87
+ // src/render.ts
88
+ var TOKEN = /\{\{\s*([\w.]+)\s*\}\}/g;
89
+ function buildScalars(ctx) {
90
+ return {
91
+ projectName: ctx.projectName,
92
+ projectSlug: ctx.projectSlug,
93
+ description: ctx.description,
94
+ // Java/Android package segments allow underscores but not hyphens.
95
+ packageId: "com.example." + ctx.projectSlug.replace(/-/g, "_"),
96
+ "apps.list": ctx.apps.join(", ")
97
+ };
98
+ }
99
+ function render(content, ctx) {
100
+ const scalars = buildScalars(ctx);
101
+ return content.replace(TOKEN, (whole, key) => {
102
+ if (Object.prototype.hasOwnProperty.call(scalars, key)) return scalars[key];
103
+ return whole;
104
+ });
105
+ }
106
+ var BINARY_EXT = /* @__PURE__ */ new Set([
107
+ ".png",
108
+ ".jpg",
109
+ ".jpeg",
110
+ ".gif",
111
+ ".webp",
112
+ ".ico",
113
+ ".ttf",
114
+ ".otf",
115
+ ".woff",
116
+ ".woff2",
117
+ ".pdf",
118
+ ".zip"
119
+ ]);
120
+ function isBinaryPath(p) {
121
+ const dot = p.lastIndexOf(".");
122
+ if (dot === -1) return false;
123
+ return BINARY_EXT.has(p.slice(dot).toLowerCase());
124
+ }
125
+ function slugify(name) {
126
+ return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
127
+ }
128
+
129
+ // src/fsutil.ts
130
+ function ensureDir(dir) {
131
+ mkdirSync(dir, { recursive: true });
132
+ }
133
+ function writeFileEnsured(p, content) {
134
+ ensureDir(dirname(p));
135
+ writeFileSync(p, content);
136
+ }
137
+ function listFiles(root) {
138
+ const out = [];
139
+ const walk = (dir) => {
140
+ for (const name of readdirSync(dir)) {
141
+ const abs = join(dir, name);
142
+ if (statSync(abs).isDirectory()) walk(abs);
143
+ else out.push(relative(root, abs));
144
+ }
145
+ };
146
+ if (existsSync(root)) walk(root);
147
+ return out;
148
+ }
149
+ function copyTree(srcRoot, destRoot, ctx) {
150
+ const written = [];
151
+ for (const rel of listFiles(srcRoot)) {
152
+ if (rel === "module.json") continue;
153
+ const src = join(srcRoot, rel);
154
+ const destRel = rel.replace(/(^|\/)_gitignore$/, "$1.gitignore");
155
+ const dest = join(destRoot, destRel);
156
+ ensureDir(dirname(dest));
157
+ if (isBinaryPath(src)) {
158
+ cpSync(src, dest);
159
+ } else {
160
+ writeFileSync(dest, render(readFileSync(src, "utf8"), ctx));
161
+ }
162
+ written.push(destRel);
163
+ }
164
+ return written;
165
+ }
166
+
167
+ // src/pm.ts
168
+ import { spawnSync } from "child_process";
169
+ var JS_PMS = ["pnpm", "npm"];
170
+ var PY_PMS = ["uv", "pip"];
171
+ var DEFAULT_JS_PM = "pnpm";
172
+ var DEFAULT_PY_PM = "uv";
173
+ var PNPM_VERSION = "pnpm@10.20.0";
174
+ function jsPmProfile(name) {
175
+ if (name === "npm") {
176
+ return {
177
+ name,
178
+ install: "npm install",
179
+ run: (pkg, script) => `npm run ${script} -w ${pkg}`,
180
+ runAll: (script) => `npm run ${script} --workspaces --if-present`,
181
+ packageManagerField: null,
182
+ usesWorkspaceYaml: false,
183
+ needsHoistNpmrc: false
184
+ };
185
+ }
186
+ return {
187
+ name: "pnpm",
188
+ install: "pnpm install",
189
+ run: (pkg, script) => `pnpm --filter ${pkg} ${script}`,
190
+ runAll: (script) => `pnpm -r ${script}`,
191
+ packageManagerField: PNPM_VERSION,
192
+ usesWorkspaceYaml: true,
193
+ needsHoistNpmrc: true
194
+ };
195
+ }
196
+ var PIP_SYNC = "python3 -m venv .venv && .venv/bin/python -m pip install -U pip && .venv/bin/python scripts/sync_deps.py";
197
+ function pyPmProfile(name) {
198
+ if (name === "pip") {
199
+ return {
200
+ name,
201
+ syncInServer: PIP_SYNC,
202
+ run: (cmd) => `.venv/bin/${cmd}`,
203
+ needsSyncScript: true
204
+ };
205
+ }
206
+ return {
207
+ name: "uv",
208
+ syncInServer: "uv sync",
209
+ run: (cmd) => `uv run ${cmd}`,
210
+ needsSyncScript: false
211
+ };
212
+ }
213
+ function hasCommand(cmd) {
214
+ try {
215
+ const probe = process.platform === "win32" ? spawnSync("where", [cmd], { stdio: "ignore" }) : spawnSync("sh", ["-c", `command -v ${cmd}`], { stdio: "ignore" });
216
+ return probe.status === 0;
217
+ } catch {
218
+ return false;
219
+ }
220
+ }
221
+ function detectJsPm() {
222
+ return hasCommand("pnpm") ? "pnpm" : "npm";
223
+ }
224
+ function detectPyPm() {
225
+ return hasCommand("uv") ? "uv" : "pip";
226
+ }
227
+
228
+ // src/rootgen.ts
229
+ import { randomBytes } from "crypto";
230
+ function jsMembers(ctx) {
231
+ const members = [];
232
+ if (ctx.hasWeb) members.push("apps/web");
233
+ if (ctx.hasMobile) members.push("apps/mobile");
234
+ if (ctx.hasShared) members.push("packages/shared");
235
+ if (ctx.hasApiClient) members.push("packages/api-client");
236
+ return members;
237
+ }
238
+ function buildJsWorkspace(ctx) {
239
+ if (!jsPmProfile(ctx.jsPm).usesWorkspaceYaml) return null;
240
+ const members = jsMembers(ctx);
241
+ if (members.length === 0) return null;
242
+ return "packages:\n" + members.map((m) => ` - "${m}"`).join("\n") + "\n";
243
+ }
244
+ function buildRootPackageJson(ctx) {
245
+ const anyJs = ctx.hasWeb || ctx.hasMobile || ctx.hasShared;
246
+ if (!anyJs) return null;
247
+ const pm = jsPmProfile(ctx.jsPm);
248
+ const scripts = {};
249
+ if (ctx.hasWeb) scripts["dev:web"] = pm.run("web", "dev");
250
+ if (ctx.hasMobile) scripts["dev:mobile"] = pm.run("mobile", "start");
251
+ if (ctx.hasApiClient) scripts["gen:api"] = pm.run("@app/api-client", "generate");
252
+ scripts["typecheck"] = pm.runAll("typecheck");
253
+ const pkg = {
254
+ name: ctx.projectSlug,
255
+ version: "0.1.0",
256
+ private: true
257
+ };
258
+ if (pm.packageManagerField) pkg.packageManager = pm.packageManagerField;
259
+ if (!pm.usesWorkspaceYaml) pkg.workspaces = jsMembers(ctx);
260
+ pkg.scripts = scripts;
261
+ pkg.devDependencies = { typescript: "^5.7.2" };
262
+ return JSON.stringify(pkg, null, 2) + "\n";
263
+ }
264
+ function buildMakefile(ctx, hasDocker) {
265
+ const js = jsPmProfile(ctx.jsPm);
266
+ const py = pyPmProfile(ctx.pyPm);
267
+ const L = [];
268
+ const phony = [];
269
+ const add = (name, body, help) => {
270
+ phony.push(name);
271
+ L.push(`${name}: ## ${help}`);
272
+ for (const line of body) L.push(` ${line}`);
273
+ L.push("");
274
+ };
275
+ L.push("# " + ctx.projectName + " \u2014 dev tasks");
276
+ L.push(".DEFAULT_GOAL := help");
277
+ L.push("");
278
+ const bootstrap = [];
279
+ if (ctx.hasWeb || ctx.hasMobile || ctx.hasShared) bootstrap.push(js.install);
280
+ if (ctx.hasServer) bootstrap.push(`cd apps/server && ${py.syncInServer}`);
281
+ if (bootstrap.length) add("bootstrap", bootstrap, "install all dependencies");
282
+ if (ctx.hasServer) {
283
+ if (hasDocker) {
284
+ add(
285
+ "db-up",
286
+ [
287
+ `@docker info >/dev/null 2>&1 || { echo "Docker isn't running \u2014 start Docker Desktop and wait for it to be ready, then retry."; exit 1; }`,
288
+ // Load the project's .env (if present) so POSTGRES_* there drive the container.
289
+ "docker compose $$([ -f .env ] && echo --env-file .env) -f infra/docker-compose.yml up -d db"
290
+ ],
291
+ "start Postgres (needs Docker running)"
292
+ );
293
+ add("db-down", ["docker compose -f infra/docker-compose.yml down"], "stop Postgres");
294
+ }
295
+ add("migrate", [`cd apps/server && ${py.run("python manage.py migrate")}`], "run migrations");
296
+ add(
297
+ "server",
298
+ [`cd apps/server && ${py.run("python manage.py runserver 0.0.0.0:8000")}`],
299
+ "run the Django dev server"
300
+ );
301
+ add(
302
+ "superuser",
303
+ [`cd apps/server && ${py.run("python manage.py createsuperuser")}`],
304
+ "create an admin user"
305
+ );
306
+ }
307
+ if (ctx.hasWeb) add("web", [js.run("web", "dev")], "run the Next.js dev server");
308
+ if (ctx.hasMobile) add("mobile", [js.run("mobile", "start")], "run the Expo dev server");
309
+ if (ctx.hasApiClient)
310
+ add("gen-api", [js.run("@app/api-client", "generate")], "regenerate the typed API client");
311
+ L.push(
312
+ "help: ## show this help",
313
+ ` @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \\033[36m%-12s\\033[0m %s\\n", $$1, $$2}'`,
314
+ "",
315
+ ".PHONY: " + [...phony, "help"].join(" "),
316
+ ""
317
+ );
318
+ return L.join("\n");
319
+ }
320
+ function buildTurboJson(ctx) {
321
+ if (!(ctx.hasWeb || ctx.hasMobile || ctx.hasShared)) return null;
322
+ return JSON.stringify(
323
+ {
324
+ $schema: "https://turbo.build/schema.json",
325
+ tasks: {
326
+ build: { dependsOn: ["^build"], outputs: ["dist/**", ".next/**"] },
327
+ dev: { cache: false, persistent: true },
328
+ typecheck: { dependsOn: ["^build"] },
329
+ lint: {}
330
+ }
331
+ },
332
+ null,
333
+ 2
334
+ ) + "\n";
335
+ }
336
+ function buildTsconfigBase(ctx) {
337
+ if (!(ctx.hasWeb || ctx.hasMobile || ctx.hasShared)) return null;
338
+ return JSON.stringify(
339
+ {
340
+ compilerOptions: {
341
+ target: "ES2022",
342
+ module: "ESNext",
343
+ moduleResolution: "Bundler",
344
+ lib: ["ES2023"],
345
+ strict: true,
346
+ esModuleInterop: true,
347
+ skipLibCheck: true,
348
+ forceConsistentCasingInFileNames: true,
349
+ resolveJsonModule: true,
350
+ isolatedModules: true
351
+ }
352
+ },
353
+ null,
354
+ 2
355
+ ) + "\n";
356
+ }
357
+ function buildNpmrc(ctx) {
358
+ if (!ctx.hasMobile) return null;
359
+ if (!jsPmProfile(ctx.jsPm).needsHoistNpmrc) return null;
360
+ return [
361
+ "# Expo / React Native need a flat node_modules layout.",
362
+ "node-linker=hoisted",
363
+ "shamefully-hoist=true",
364
+ ""
365
+ ].join("\n");
366
+ }
367
+ function buildBaseEnv(ctx) {
368
+ const lines = [
369
+ "# Environment for " + ctx.projectName,
370
+ "#",
371
+ "# Every value the app reads from the environment lives here. Copy this file to",
372
+ "# `.env` and adjust as needed \u2014 the server, web/mobile apps, and each component",
373
+ "# read their configuration from it, so there's nothing to configure elsewhere.",
374
+ ""
375
+ ];
376
+ if (ctx.hasServer) {
377
+ lines.push(
378
+ "# --- server ---",
379
+ "# Unique per-project key generated at scaffold time; signs sessions and JWTs.",
380
+ "# Use a separate secret value in production (and never commit the real one).",
381
+ `DJANGO_SECRET_KEY=${randomBytes(48).toString("base64url")}`,
382
+ "DJANGO_DEBUG=true",
383
+ "DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1",
384
+ "# Web origins allowed to call the API when DEBUG is off (comma-separated).",
385
+ "# In DEBUG every origin is allowed, so this is only needed in production.",
386
+ "# DJANGO_CORS_ALLOWED_ORIGINS=https://app.example.com",
387
+ "# DATABASE_URL \u2014 the server uses local SQLite by default; set a database URL",
388
+ "# to switch (the `db-postgres` component sets a Postgres DSN here for you).",
389
+ ""
390
+ );
391
+ }
392
+ if (ctx.hasWeb) {
393
+ lines.push("# --- web ---", "NEXT_PUBLIC_API_URL=http://localhost:8000", "");
394
+ }
395
+ if (ctx.hasMobile) {
396
+ lines.push(
397
+ "# --- mobile (Expo) ---",
398
+ "# In dev the app auto-detects your machine's LAN IP so a physical device can",
399
+ "# reach the server \u2014 leave this unset. Set it for simulators or production,",
400
+ "# in the environment or apps/mobile/.env (Expo reads EXPO_PUBLIC_* from there).",
401
+ "# EXPO_PUBLIC_API_URL=http://localhost:8000",
402
+ ""
403
+ );
404
+ }
405
+ return lines.join("\n");
406
+ }
407
+ function jsCiSteps(ctx) {
408
+ if (ctx.jsPm === "npm") {
409
+ return ` - uses: actions/setup-node@v4
410
+ with:
411
+ node-version: 20
412
+ - run: npm install`;
413
+ }
414
+ return ` - uses: pnpm/action-setup@v4
415
+ - uses: actions/setup-node@v4
416
+ with:
417
+ node-version: 20
418
+ cache: pnpm
419
+ - run: pnpm install --frozen-lockfile`;
420
+ }
421
+ function serverCiSteps(ctx) {
422
+ const py = pyPmProfile(ctx.pyPm);
423
+ if (ctx.pyPm === "pip") {
424
+ return ` - uses: actions/setup-python@v5
425
+ with:
426
+ python-version: "3.12"
427
+ - run: ${py.syncInServer}
428
+ - run: ${py.run("ruff check .")}
429
+ - run: ${py.run("python manage.py migrate")}
430
+ - run: ${py.run("pytest -q")}`;
431
+ }
432
+ return ` - uses: astral-sh/setup-uv@v5
433
+ - run: ${py.syncInServer}
434
+ - run: ${py.run("ruff check .")}
435
+ - run: ${py.run("python manage.py migrate")}
436
+ - run: ${py.run("pytest -q")}`;
437
+ }
438
+ function buildCiWorkflows(ctx) {
439
+ const js = jsPmProfile(ctx.jsPm);
440
+ const out = {};
441
+ if (ctx.hasServer) {
442
+ out[".github/workflows/server.yml"] = `name: server
443
+ on:
444
+ push:
445
+ paths: ["apps/server/**", ".github/workflows/server.yml"]
446
+ pull_request:
447
+ paths: ["apps/server/**", ".github/workflows/server.yml"]
448
+ jobs:
449
+ test:
450
+ runs-on: ubuntu-latest
451
+ defaults:
452
+ run:
453
+ working-directory: apps/server
454
+ steps:
455
+ - uses: actions/checkout@v4
456
+ ${serverCiSteps(ctx)}
457
+ `;
458
+ }
459
+ if (ctx.hasWeb) {
460
+ out[".github/workflows/web.yml"] = `name: web
461
+ on:
462
+ push:
463
+ paths: ["apps/web/**", "packages/**", ".github/workflows/web.yml"]
464
+ pull_request:
465
+ paths: ["apps/web/**", "packages/**", ".github/workflows/web.yml"]
466
+ jobs:
467
+ build:
468
+ runs-on: ubuntu-latest
469
+ steps:
470
+ - uses: actions/checkout@v4
471
+ ${jsCiSteps(ctx)}
472
+ - run: ${js.run("web", "typecheck")}
473
+ - run: ${js.run("web", "build")}
474
+ `;
475
+ }
476
+ if (ctx.hasMobile) {
477
+ out[".github/workflows/mobile.yml"] = `name: mobile
478
+ on:
479
+ push:
480
+ paths: ["apps/mobile/**", "packages/**", ".github/workflows/mobile.yml"]
481
+ pull_request:
482
+ paths: ["apps/mobile/**", "packages/**", ".github/workflows/mobile.yml"]
483
+ jobs:
484
+ typecheck:
485
+ runs-on: ubuntu-latest
486
+ steps:
487
+ - uses: actions/checkout@v4
488
+ ${jsCiSteps(ctx)}
489
+ - run: ${js.run("mobile", "typecheck")}
490
+ `;
491
+ }
492
+ return out;
493
+ }
494
+ function buildReadme(ctx, modules) {
495
+ const parts = [];
496
+ parts.push(`# ${ctx.projectName}`, "", ctx.description, "");
497
+ parts.push("Generated with **partweave** \u2014 a modular full-stack scaffolder.", "");
498
+ parts.push("## What's inside", "");
499
+ if (ctx.hasServer) parts.push(`- \`apps/server\` \u2014 Django + DRF API (managed by \`${ctx.pyPm}\`)`);
500
+ if (ctx.hasWeb) parts.push("- `apps/web` \u2014 Next.js web app");
501
+ if (ctx.hasMobile) parts.push("- `apps/mobile` \u2014 Expo (React Native) app");
502
+ if (ctx.hasShared) parts.push("- `packages/shared` \u2014 shared TypeScript interfaces & schemas");
503
+ if (ctx.hasApiClient) parts.push("- `packages/api-client` \u2014 typed client generated from the API's OpenAPI schema");
504
+ parts.push("");
505
+ if (modules.length) {
506
+ parts.push("### Components", "");
507
+ for (const m of modules) parts.push(`- **${m.manifest.title}** \u2014 ${m.manifest.description ?? ""}`);
508
+ parts.push("");
509
+ }
510
+ parts.push("## Getting started", "", "```sh", "make bootstrap");
511
+ if (ctx.hasServer) parts.push("make db-up && make migrate", "make server # http://localhost:8000");
512
+ if (ctx.hasWeb) parts.push("make web # http://localhost:3000");
513
+ if (ctx.hasMobile) parts.push("make mobile # Expo dev server");
514
+ parts.push("```", "");
515
+ parts.push("Copy `.env.example` to `.env` and fill in values before running.", "");
516
+ return parts.join("\n");
517
+ }
518
+ function buildServerDockerfile(ctx) {
519
+ if (ctx.pyPm === "pip") {
520
+ return `# Production image for the Django server (pip / venv).
521
+ FROM python:3.12-slim
522
+
523
+ ENV PYTHONUNBUFFERED=1 \\
524
+ PYTHONDONTWRITEBYTECODE=1
525
+
526
+ WORKDIR /app
527
+
528
+ COPY . .
529
+ RUN python3 -m venv .venv \\
530
+ && .venv/bin/python -m pip install -U pip \\
531
+ && .venv/bin/python scripts/sync_deps.py --no-dev
532
+
533
+ EXPOSE 8000
534
+ CMD [".venv/bin/gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
535
+ `;
536
+ }
537
+ return `# Production image for the Django server, built with uv.
538
+ FROM python:3.12-slim
539
+
540
+ ENV PYTHONUNBUFFERED=1 \\
541
+ PYTHONDONTWRITEBYTECODE=1 \\
542
+ UV_COMPILE_BYTECODE=1 \\
543
+ UV_LINK_MODE=copy
544
+
545
+ WORKDIR /app
546
+
547
+ # uv binary from the official distroless image
548
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
549
+
550
+ COPY . .
551
+ RUN uv sync --no-dev
552
+
553
+ EXPOSE 8000
554
+ CMD ["uv", "run", "gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
555
+ `;
556
+ }
557
+ function buildPipSyncScript(ctx) {
558
+ if (!ctx.hasServer || !pyPmProfile(ctx.pyPm).needsSyncScript) return null;
559
+ return `#!/usr/bin/env python3
560
+ """Install this app's dependencies from pyproject.toml into the active venv.
561
+
562
+ The pip/venv counterpart to \`uv sync\`. Run it (via the venv's Python) after
563
+ adding a component so new dependencies are picked up:
564
+
565
+ .venv/bin/python scripts/sync_deps.py # runtime + dev deps
566
+ .venv/bin/python scripts/sync_deps.py --no-dev # runtime deps only (prod)
567
+ """
568
+ import subprocess
569
+ import sys
570
+ import tomllib
571
+ from pathlib import Path
572
+
573
+ no_dev = "--no-dev" in sys.argv[1:]
574
+ pyproject = Path(__file__).resolve().parent.parent / "pyproject.toml"
575
+ data = tomllib.loads(pyproject.read_text())
576
+ deps = list(data.get("project", {}).get("dependencies", []))
577
+ if not no_dev:
578
+ deps += data.get("dependency-groups", {}).get("dev", [])
579
+ if deps:
580
+ subprocess.check_call([sys.executable, "-m", "pip", "install", *deps])
581
+ `;
582
+ }
583
+
584
+ // src/types.ts
585
+ import { z } from "zod";
586
+ var TARGETS = [
587
+ "root",
588
+ "server",
589
+ "web",
590
+ "mobile",
591
+ "shared",
592
+ "api-client"
593
+ ];
594
+ var TARGET_DEST = {
595
+ root: ".",
596
+ server: "apps/server",
597
+ web: "apps/web",
598
+ mobile: "apps/mobile",
599
+ shared: "packages/shared",
600
+ "api-client": "packages/api-client"
601
+ };
602
+ var APPS = ["server", "web", "mobile"];
603
+ var WiringForTargetSchema = z.object({
604
+ installedApps: z.array(z.string()).optional(),
605
+ // server → <partweave:installed-apps>
606
+ urls: z.array(z.string()).optional(),
607
+ // server → <partweave:urls>
608
+ settings: z.array(z.string()).optional(),
609
+ // server → <partweave:settings>
610
+ providers: z.array(z.string()).optional(),
611
+ // web/mobile → <partweave:providers>
612
+ routes: z.array(z.string()).optional(),
613
+ // web/mobile → <partweave:routes>
614
+ deps: z.array(z.string()).optional(),
615
+ // merged into pyproject/package.json
616
+ anchors: z.record(z.array(z.string())).optional()
617
+ // generic anchorId → lines
618
+ });
619
+ var CONVENIENCE_ANCHORS = {
620
+ installedApps: "installed-apps",
621
+ urls: "urls",
622
+ settings: "settings",
623
+ providers: "providers",
624
+ routes: "routes"
625
+ };
626
+ var ManifestSchema = z.object({
627
+ id: z.string().regex(/^[a-z0-9][a-z0-9-]*$/, "id must be kebab-case"),
628
+ title: z.string(),
629
+ description: z.string().optional(),
630
+ /** "app" = a top-level toggle (server/web/mobile); "feature" = a component. */
631
+ kind: z.enum(["app", "feature"]).default("feature"),
632
+ /** which target sub-projects this module contributes to */
633
+ targets: z.array(z.enum(TARGETS)).min(1),
634
+ /** apps that MUST be selected for this module to work (e.g. auth needs server) */
635
+ requiresApps: z.array(z.enum(APPS)).default([]),
636
+ /** modules that must also be present (auto-selected) */
637
+ requires: z.array(z.string()).default([]),
638
+ /** modules that cannot coexist with this one */
639
+ conflicts: z.array(z.string()).default([]),
640
+ /** capability/interface this module satisfies (for conflict grouping) */
641
+ provides: z.string().optional(),
642
+ /** env keys → default value, appended to .env.example */
643
+ env: z.record(z.string()).default({}),
644
+ /** per-target wiring (files are copied separately from wiring injection) */
645
+ wiring: z.record(z.enum(TARGETS), WiringForTargetSchema).default({}),
646
+ /** whether this module is offered by default in the interactive picker */
647
+ default: z.boolean().default(false),
648
+ /** notes printed after generation */
649
+ notes: z.array(z.string()).default([])
650
+ });
651
+
652
+ // src/compose.ts
653
+ function buildContext(selection) {
654
+ const hasServer = selection.apps.includes("server");
655
+ const hasWeb = selection.apps.includes("web");
656
+ const hasMobile = selection.apps.includes("mobile");
657
+ const hasShared = hasWeb || hasMobile;
658
+ const hasApiClient = hasServer && (hasWeb || hasMobile);
659
+ return {
660
+ projectName: selection.projectName,
661
+ projectSlug: slugify(selection.projectName),
662
+ description: `${selection.projectName} \u2014 generated with partweave.`,
663
+ apps: selection.apps,
664
+ hasServer,
665
+ hasWeb,
666
+ hasMobile,
667
+ hasShared,
668
+ hasApiClient,
669
+ jsPm: selection.jsPm ?? DEFAULT_JS_PM,
670
+ pyPm: selection.pyPm ?? DEFAULT_PY_PM
671
+ };
672
+ }
673
+ function selectedTargets(ctx) {
674
+ const t = /* @__PURE__ */ new Set(["root"]);
675
+ if (ctx.hasServer) t.add("server");
676
+ if (ctx.hasWeb) t.add("web");
677
+ if (ctx.hasMobile) t.add("mobile");
678
+ if (ctx.hasShared) t.add("shared");
679
+ if (ctx.hasApiClient) t.add("api-client");
680
+ return t;
681
+ }
682
+ var COPY_ORDER = [
683
+ "root",
684
+ "server",
685
+ "web",
686
+ "mobile",
687
+ "shared",
688
+ "api-client"
689
+ ];
690
+ function buildAnchorIndex(dir) {
691
+ const index = /* @__PURE__ */ new Map();
692
+ const re = /<partweave:([\w-]+)>/g;
693
+ for (const rel of listFiles(dir)) {
694
+ const abs = join2(dir, rel);
695
+ if (isBinaryPath(abs)) continue;
696
+ const content = readFileSync2(abs, "utf8");
697
+ let m;
698
+ while (m = re.exec(content)) {
699
+ const id = m[1];
700
+ const arr = index.get(id) ?? [];
701
+ if (!arr.includes(abs)) arr.push(abs);
702
+ index.set(id, arr);
703
+ }
704
+ }
705
+ return index;
706
+ }
707
+ function anchorInjections(w) {
708
+ const out = {};
709
+ const push = (anchor, lines) => {
710
+ if (!lines?.length) return;
711
+ out[anchor] = [...out[anchor] ?? [], ...lines];
712
+ };
713
+ for (const [field, anchor] of Object.entries(CONVENIENCE_ANCHORS)) {
714
+ push(anchor, w[field]);
715
+ }
716
+ if (w.anchors) for (const [a, lines] of Object.entries(w.anchors)) push(a, lines);
717
+ return out;
718
+ }
719
+ function injectIntoFiles(index, anchorId, lines, targetLabel) {
720
+ const files = index.get(anchorId);
721
+ if (!files || files.length === 0) {
722
+ throw new Error(
723
+ `Wiring error: anchor <partweave:${anchorId}> not found in ${targetLabel}. Add the anchor to the _core/${targetLabel} scaffold.`
724
+ );
725
+ }
726
+ for (const file of files) {
727
+ const content = readFileSync2(file, "utf8");
728
+ const { content: next } = injectAtAnchor(content, anchorId, lines);
729
+ writeFileSync2(file, next);
730
+ }
731
+ }
732
+ function applyWiring(outDir, targets, modules) {
733
+ const indexes = /* @__PURE__ */ new Map();
734
+ for (const t of targets) {
735
+ indexes.set(t, buildAnchorIndex(join2(outDir, TARGET_DEST[t])));
736
+ }
737
+ for (const mod of modules) {
738
+ for (const t of mod.manifest.targets) {
739
+ if (!targets.has(t)) continue;
740
+ const wiring = mod.manifest.wiring[t];
741
+ if (!wiring) continue;
742
+ const index = indexes.get(t);
743
+ const targetDir = join2(outDir, TARGET_DEST[t]);
744
+ for (const [anchor, lines] of Object.entries(anchorInjections(wiring))) {
745
+ injectIntoFiles(index, anchor, lines, t);
746
+ }
747
+ if (wiring.deps?.length) {
748
+ if (t === "server") {
749
+ const lines = wiring.deps.map((d) => `"${d}",`);
750
+ injectIntoFiles(index, "deps", lines, t);
751
+ } else {
752
+ const pkgPath = join2(targetDir, "package.json");
753
+ if (existsSync2(pkgPath)) {
754
+ const merged = mergePackageJsonDeps(
755
+ readFileSync2(pkgPath, "utf8"),
756
+ wiring.deps
757
+ );
758
+ writeFileSync2(pkgPath, merged);
759
+ }
760
+ }
761
+ }
762
+ }
763
+ }
764
+ }
765
+ function applyEnv(outDir, modules) {
766
+ const envPath = join2(outDir, ".env.example");
767
+ let body = existsSync2(envPath) ? readFileSync2(envPath, "utf8") : "";
768
+ for (const mod of modules) {
769
+ if (Object.keys(mod.manifest.env).length === 0) continue;
770
+ body = appendEnv(body, mod.manifest.env, mod.manifest.title);
771
+ }
772
+ if (body) writeFileSync2(envPath, body);
773
+ }
774
+ function writeStructuralRootFiles(outDir, ctx, modules) {
775
+ const workspace = buildJsWorkspace(ctx);
776
+ if (workspace) writeFileEnsured(join2(outDir, "pnpm-workspace.yaml"), workspace);
777
+ const rootPkg = buildRootPackageJson(ctx);
778
+ if (rootPkg) writeFileEnsured(join2(outDir, "package.json"), rootPkg);
779
+ const turbo = buildTurboJson(ctx);
780
+ if (turbo) writeFileEnsured(join2(outDir, "turbo.json"), turbo);
781
+ const tsbase = buildTsconfigBase(ctx);
782
+ if (tsbase) writeFileEnsured(join2(outDir, "tsconfig.base.json"), tsbase);
783
+ const npmrc = buildNpmrc(ctx);
784
+ if (npmrc) writeFileEnsured(join2(outDir, ".npmrc"), npmrc);
785
+ const pipSync = buildPipSyncScript(ctx);
786
+ if (pipSync) writeFileEnsured(join2(outDir, "apps/server/scripts/sync_deps.py"), pipSync);
787
+ const hasDocker = modules.some((m) => m.manifest.id === "docker");
788
+ writeFileEnsured(join2(outDir, "Makefile"), buildMakefile(ctx, hasDocker));
789
+ writeFileEnsured(join2(outDir, ".env.example"), buildBaseEnv(ctx));
790
+ }
791
+ function compose(opts) {
792
+ const { selection, registry, scaffoldTargets, wireTargets, rootFiles } = opts;
793
+ const ctx = buildContext(selection);
794
+ const outDir = selection.outDir;
795
+ const modules = selection.modules.map((id) => registry.require(id));
796
+ const written = [];
797
+ for (const t of COPY_ORDER) {
798
+ if (!scaffoldTargets.has(t)) continue;
799
+ const src = join2(registry.modulesDir, "_core", t);
800
+ if (existsSync2(src)) {
801
+ written.push(...copyTree(src, join2(outDir, TARGET_DEST[t]), ctx));
802
+ }
803
+ }
804
+ if (rootFiles !== "none") writeStructuralRootFiles(outDir, ctx, modules);
805
+ if (rootFiles === "all") {
806
+ writeFileEnsured(join2(outDir, "README.md"), buildReadme(ctx, modules));
807
+ }
808
+ for (const mod of modules) {
809
+ for (const t of mod.manifest.targets) {
810
+ if (!wireTargets.has(t)) continue;
811
+ const src = join2(mod.dir, t);
812
+ if (existsSync2(src)) {
813
+ written.push(...copyTree(src, join2(outDir, TARGET_DEST[t]), ctx));
814
+ }
815
+ }
816
+ }
817
+ applyWiring(outDir, wireTargets, modules);
818
+ applyEnv(outDir, modules);
819
+ if (selection.modules.includes("ci")) {
820
+ for (const [rel, content] of Object.entries(buildCiWorkflows(ctx))) {
821
+ writeFileEnsured(join2(outDir, rel), content);
822
+ written.push(rel);
823
+ }
824
+ }
825
+ if (selection.modules.includes("docker") && ctx.hasServer) {
826
+ const rel = "apps/server/Dockerfile";
827
+ writeFileEnsured(join2(outDir, rel), buildServerDockerfile(ctx));
828
+ written.push(rel);
829
+ }
830
+ const notes = modules.flatMap((m) => m.manifest.notes);
831
+ return { written, notes };
832
+ }
833
+
834
+ // src/registry.ts
835
+ import { readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
836
+ import { join as join4 } from "path";
837
+
838
+ // src/paths.ts
839
+ import { existsSync as existsSync3 } from "fs";
840
+ import { dirname as dirname2, join as join3, resolve } from "path";
841
+ import { fileURLToPath } from "url";
842
+ var here = dirname2(fileURLToPath(import.meta.url));
843
+ function findModulesDir() {
844
+ const override = process.env.PARTWEAVE_MODULES_DIR;
845
+ if (override) {
846
+ const abs = resolve(override);
847
+ if (existsSync3(join3(abs, "_core"))) return abs;
848
+ throw new Error(`PARTWEAVE_MODULES_DIR=${override} has no _core/ inside it`);
849
+ }
850
+ let cur = here;
851
+ for (let i = 0; i < 8; i++) {
852
+ const candidate = join3(cur, "modules");
853
+ if (existsSync3(join3(candidate, "_core"))) return candidate;
854
+ const parent = dirname2(cur);
855
+ if (parent === cur) break;
856
+ cur = parent;
857
+ }
858
+ throw new Error(
859
+ "Could not locate the modules/ catalog. Set PARTWEAVE_MODULES_DIR to point at it."
860
+ );
861
+ }
862
+
863
+ // src/registry.ts
864
+ var Registry = class {
865
+ modulesDir;
866
+ byId = /* @__PURE__ */ new Map();
867
+ constructor(modulesDir = findModulesDir()) {
868
+ this.modulesDir = modulesDir;
869
+ this.load();
870
+ }
871
+ load() {
872
+ for (const name of readdirSync2(this.modulesDir)) {
873
+ if (name === "_core" || name.startsWith(".")) continue;
874
+ const dir = join4(this.modulesDir, name);
875
+ if (!statSync2(dir).isDirectory()) continue;
876
+ const manifestPath = join4(dir, "module.json");
877
+ let raw;
878
+ try {
879
+ raw = JSON.parse(readFileSync3(manifestPath, "utf8"));
880
+ } catch {
881
+ throw new Error(`Module "${name}" is missing a valid module.json`);
882
+ }
883
+ const parsed = ManifestSchema.safeParse(raw);
884
+ if (!parsed.success) {
885
+ throw new Error(
886
+ `Invalid module.json for "${name}":
887
+ ${parsed.error.toString()}`
888
+ );
889
+ }
890
+ if (parsed.data.id !== name) {
891
+ throw new Error(
892
+ `Module dir "${name}" does not match manifest id "${parsed.data.id}"`
893
+ );
894
+ }
895
+ this.byId.set(parsed.data.id, { manifest: parsed.data, dir });
896
+ }
897
+ }
898
+ get(id) {
899
+ return this.byId.get(id);
900
+ }
901
+ require(id) {
902
+ const m = this.byId.get(id);
903
+ if (!m) throw new Error(`Unknown module: "${id}"`);
904
+ return m;
905
+ }
906
+ has(id) {
907
+ return this.byId.has(id);
908
+ }
909
+ all() {
910
+ return [...this.byId.values()];
911
+ }
912
+ /** Feature modules (not app-kind), sorted for stable display. */
913
+ features() {
914
+ return this.all().filter((m) => m.manifest.kind === "feature").sort((a, b) => a.manifest.id.localeCompare(b.manifest.id));
915
+ }
916
+ };
917
+
918
+ // src/projectmanifest.ts
919
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
920
+ import { join as join5 } from "path";
921
+ var REL = ".partweave/manifest.json";
922
+ function readProjectManifest(dir) {
923
+ const p = join5(dir, REL);
924
+ if (!existsSync4(p)) return null;
925
+ try {
926
+ return JSON.parse(readFileSync4(p, "utf8"));
927
+ } catch {
928
+ return null;
929
+ }
930
+ }
931
+ function writeProjectManifest(dir, pm) {
932
+ writeFileEnsured(join5(dir, REL), JSON.stringify(pm, null, 2) + "\n");
933
+ }
934
+
935
+ // src/resolve.ts
936
+ function resolveModules(registry, chosen) {
937
+ const requested = new Set(chosen);
938
+ const resolved = /* @__PURE__ */ new Set();
939
+ const autoAdded = /* @__PURE__ */ new Set();
940
+ const visit = (id, stack) => {
941
+ if (resolved.has(id)) return;
942
+ if (stack.includes(id)) {
943
+ throw new Error(
944
+ `Circular module dependency: ${[...stack, id].join(" \u2192 ")}`
945
+ );
946
+ }
947
+ const mod = registry.get(id);
948
+ if (!mod) {
949
+ throw new Error(`Unknown module: "${id}"`);
950
+ }
951
+ for (const dep of mod.manifest.requires) {
952
+ if (!requested.has(dep)) autoAdded.add(dep);
953
+ visit(dep, [...stack, id]);
954
+ }
955
+ resolved.add(id);
956
+ };
957
+ for (const id of chosen) visit(id, []);
958
+ const provided = /* @__PURE__ */ new Map();
959
+ for (const id of resolved) {
960
+ const mod = registry.require(id);
961
+ for (const c of mod.manifest.conflicts) {
962
+ if (resolved.has(c)) {
963
+ throw new Error(`Modules "${id}" and "${c}" conflict and cannot both be selected.`);
964
+ }
965
+ }
966
+ const cap = mod.manifest.provides;
967
+ if (cap) {
968
+ const other = provided.get(cap);
969
+ if (other && other !== id) {
970
+ throw new Error(
971
+ `Modules "${id}" and "${other}" both provide "${cap}"; pick one.`
972
+ );
973
+ }
974
+ provided.set(cap, id);
975
+ }
976
+ }
977
+ const order = [...resolved].sort((a, b) => {
978
+ const da = registry.require(a).manifest.requires.length;
979
+ const db = registry.require(b).manifest.requires.length;
980
+ return da - db || a.localeCompare(b);
981
+ });
982
+ return { modules: order, autoAdded: [...autoAdded].sort() };
983
+ }
984
+ function validateApps(registry, moduleIds, apps) {
985
+ const have = new Set(apps);
986
+ const problems = [];
987
+ for (const id of moduleIds) {
988
+ const mod = registry.require(id);
989
+ for (const app of mod.manifest.requiresApps) {
990
+ if (!have.has(app)) {
991
+ problems.push(`"${mod.manifest.id}" needs the ${app} app`);
992
+ }
993
+ }
994
+ }
995
+ if (problems.length) {
996
+ throw new Error(
997
+ `Incompatible selection:
998
+ - ${problems.join("\n - ")}
999
+ Enable the required app(s) or drop the component(s).`
1000
+ );
1001
+ }
1002
+ }
1003
+
1004
+ // src/prompts.ts
1005
+ import {
1006
+ cancel,
1007
+ confirm,
1008
+ isCancel,
1009
+ multiselect,
1010
+ select,
1011
+ text
1012
+ } from "@clack/prompts";
1013
+ import pc from "picocolors";
1014
+ import { resolve as resolve2 } from "path";
1015
+ var MULTI_HINT = pc.dim("\u2191/\u2193 move \xB7 space toggle \xB7 a all \xB7 enter confirm");
1016
+ function bail(v) {
1017
+ if (isCancel(v)) {
1018
+ cancel("Cancelled.");
1019
+ process.exit(0);
1020
+ }
1021
+ }
1022
+ async function promptCreate(registry, defaults) {
1023
+ const nameRaw = await text({
1024
+ message: "Project name",
1025
+ placeholder: "my-project",
1026
+ initialValue: defaults.projectName ?? "",
1027
+ validate: (v) => v.trim() ? void 0 : "Required"
1028
+ });
1029
+ bail(nameRaw);
1030
+ const projectName = nameRaw.trim();
1031
+ const dirRaw = await text({
1032
+ message: "Target directory",
1033
+ initialValue: defaults.outDir ?? `./${slugify(projectName)}`,
1034
+ validate: (v) => v.trim() ? void 0 : "Required"
1035
+ });
1036
+ bail(dirRaw);
1037
+ const outDir = resolve2(dirRaw.trim());
1038
+ const appsRaw = await multiselect({
1039
+ message: `Which apps? ${MULTI_HINT}`,
1040
+ options: [
1041
+ { value: "server", label: "Server", hint: "Django + DRF" },
1042
+ { value: "web", label: "Web", hint: "Next.js" },
1043
+ { value: "mobile", label: "Mobile", hint: "Expo / React Native" }
1044
+ ],
1045
+ initialValues: defaults.apps ?? [...APPS],
1046
+ required: true
1047
+ });
1048
+ bail(appsRaw);
1049
+ const apps = appsRaw;
1050
+ const present = new Set(apps);
1051
+ if (apps.includes("web") || apps.includes("mobile")) present.add("shared");
1052
+ const relevant = registry.features().filter((m) => m.manifest.targets.some((t) => present.has(t))).filter((m) => m.manifest.requiresApps.every((a) => apps.includes(a)));
1053
+ let modules = [];
1054
+ if (relevant.length) {
1055
+ const modsRaw = await multiselect({
1056
+ message: `Which components? (deps auto-added) ${MULTI_HINT}`,
1057
+ options: relevant.map((m) => ({
1058
+ value: m.manifest.id,
1059
+ label: m.manifest.title,
1060
+ hint: m.manifest.description
1061
+ })),
1062
+ initialValues: relevant.filter((m) => m.manifest.default).map((m) => m.manifest.id),
1063
+ required: false
1064
+ });
1065
+ bail(modsRaw);
1066
+ modules = modsRaw;
1067
+ }
1068
+ const anyJs = apps.includes("web") || apps.includes("mobile");
1069
+ let jsPm = defaults.jsPm ?? detectJsPm();
1070
+ if (anyJs) {
1071
+ const jsRaw = await select({
1072
+ message: "JS/TS package manager",
1073
+ initialValue: jsPm,
1074
+ options: [
1075
+ { value: "pnpm", label: "pnpm", hint: hasCommand("pnpm") ? "detected" : "not found on PATH" },
1076
+ { value: "npm", label: "npm", hint: "ships with Node" }
1077
+ ]
1078
+ });
1079
+ bail(jsRaw);
1080
+ jsPm = jsRaw;
1081
+ }
1082
+ let pyPm = defaults.pyPm ?? detectPyPm();
1083
+ if (apps.includes("server")) {
1084
+ const pyRaw = await select({
1085
+ message: "Python package manager (server)",
1086
+ initialValue: pyPm,
1087
+ options: [
1088
+ { value: "uv", label: "uv", hint: hasCommand("uv") ? "detected \xB7 fast" : "not found on PATH" },
1089
+ { value: "pip", label: "pip + venv", hint: "ships with Python" }
1090
+ ]
1091
+ });
1092
+ bail(pyRaw);
1093
+ pyPm = pyRaw;
1094
+ }
1095
+ const ok = await confirm({
1096
+ message: `Scaffold ${apps.join(" + ")}${modules.length ? " with " + modules.join(", ") : ""} into ${outDir}?`
1097
+ });
1098
+ bail(ok);
1099
+ if (!ok) {
1100
+ cancel("Cancelled.");
1101
+ process.exit(0);
1102
+ }
1103
+ return { projectName, outDir, apps, modules, jsPm, pyPm };
1104
+ }
1105
+
1106
+ // src/commands/create.ts
1107
+ function resolvePm(value, allowed, detect, flag) {
1108
+ if (value === void 0) return detect();
1109
+ if (allowed.includes(value)) return value;
1110
+ log.error(`Invalid ${flag} "${value}". Choose one of: ${allowed.join(", ")}.`);
1111
+ process.exit(1);
1112
+ }
1113
+ function appsFromFlags(flags) {
1114
+ const explicit = ["server", "web", "mobile"].some(
1115
+ (k) => flags[k] !== void 0
1116
+ );
1117
+ if (!explicit) return null;
1118
+ const apps = APPS.filter((a) => flags[a] === true);
1119
+ if (apps.length === 0) return APPS.filter((a) => flags[a] !== false);
1120
+ return apps;
1121
+ }
1122
+ function defaultModules(registry, apps) {
1123
+ const present = new Set(apps);
1124
+ if (apps.includes("web") || apps.includes("mobile")) present.add("shared");
1125
+ 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);
1126
+ }
1127
+ async function runCreate(flags) {
1128
+ intro(pc2.bgCyan(pc2.black(" partweave ")) + pc2.dim(" full-stack scaffolder"));
1129
+ const registry = new Registry();
1130
+ const flagApps = appsFromFlags(flags);
1131
+ const nonInteractive = flags.yes === true || flagApps !== null;
1132
+ const jsPm = resolvePm(flags.jsPm, JS_PMS, detectJsPm, "--js-pm");
1133
+ const pyPm = resolvePm(flags.pyPm, PY_PMS, detectPyPm, "--py-pm");
1134
+ let choices;
1135
+ if (nonInteractive) {
1136
+ const apps = flagApps ?? [...APPS];
1137
+ const name = flags.name ?? "my-app";
1138
+ const outDir = resolve3(flags.dir ?? `./${slugify(name)}`);
1139
+ const modules = flags.with !== void 0 ? flags.with.split(",").map((s2) => s2.trim()).filter(Boolean) : defaultModules(registry, apps);
1140
+ choices = { projectName: name, outDir, apps, modules, jsPm, pyPm };
1141
+ } else {
1142
+ choices = await promptCreate(registry, {
1143
+ projectName: flags.name,
1144
+ outDir: flags.dir ? resolve3(flags.dir) : void 0,
1145
+ // pre-select an explicitly-passed --js-pm/--py-pm; otherwise the prompt
1146
+ // defaults to whatever is installed.
1147
+ jsPm: flags.jsPm ? jsPm : void 0,
1148
+ pyPm: flags.pyPm ? pyPm : void 0
1149
+ });
1150
+ }
1151
+ if (existsSync5(choices.outDir) && readdirSync3(choices.outDir).length > 0 && !flags.force) {
1152
+ log.error(`${choices.outDir} exists and is not empty. Use --force to override.`);
1153
+ process.exit(1);
1154
+ }
1155
+ let resolved;
1156
+ try {
1157
+ resolved = resolveModules(registry, choices.modules);
1158
+ } catch (err) {
1159
+ log.error(err.message);
1160
+ process.exit(1);
1161
+ }
1162
+ if (resolved.autoAdded.length) {
1163
+ log.info(`Added required components: ${resolved.autoAdded.join(", ")}`);
1164
+ }
1165
+ try {
1166
+ validateApps(registry, resolved.modules, choices.apps);
1167
+ } catch (err) {
1168
+ log.error(err.message);
1169
+ process.exit(1);
1170
+ }
1171
+ const s = spinner();
1172
+ s.start("Scaffolding");
1173
+ let result;
1174
+ try {
1175
+ const selection = {
1176
+ projectName: choices.projectName,
1177
+ outDir: choices.outDir,
1178
+ apps: choices.apps,
1179
+ modules: resolved.modules,
1180
+ jsPm: choices.jsPm,
1181
+ pyPm: choices.pyPm
1182
+ };
1183
+ const targets = selectedTargets(buildContext(selection));
1184
+ result = compose({
1185
+ selection,
1186
+ registry,
1187
+ scaffoldTargets: targets,
1188
+ wireTargets: targets,
1189
+ rootFiles: "all"
1190
+ });
1191
+ } catch (err) {
1192
+ s.stop("Failed");
1193
+ log.error(err.message);
1194
+ process.exit(1);
1195
+ }
1196
+ s.stop(`Created ${result.written.length} files`);
1197
+ writeProjectManifest(choices.outDir, {
1198
+ name: choices.projectName,
1199
+ apps: choices.apps,
1200
+ modules: resolved.modules,
1201
+ jsPm: choices.jsPm,
1202
+ pyPm: choices.pyPm
1203
+ });
1204
+ const rel = basename(choices.outDir);
1205
+ const steps = [`cd ${rel}`, "make bootstrap"];
1206
+ if (choices.apps.includes("server")) steps.push("make db-up && make migrate", "make server");
1207
+ if (choices.apps.includes("web")) steps.push("make web");
1208
+ if (choices.apps.includes("mobile")) steps.push("make mobile");
1209
+ note(steps.join("\n"), "Next steps");
1210
+ if (result.notes.length) note(result.notes.join("\n"), "Notes");
1211
+ outro(pc2.green("Done."));
1212
+ }
1213
+
1214
+ export {
1215
+ DEFAULT_JS_PM,
1216
+ DEFAULT_PY_PM,
1217
+ jsPmProfile,
1218
+ pyPmProfile,
1219
+ APPS,
1220
+ buildContext,
1221
+ selectedTargets,
1222
+ compose,
1223
+ Registry,
1224
+ readProjectManifest,
1225
+ writeProjectManifest,
1226
+ resolveModules,
1227
+ validateApps,
1228
+ runCreate
1229
+ };
1230
+ //# sourceMappingURL=chunk-2IGBGILB.js.map