partweave 0.3.0 → 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.
package/LICENSE CHANGED
@@ -1,21 +1,201 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Agrim Sigdel
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or Derivative
95
+ Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Agrim Sigdel
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/NOTICE ADDED
@@ -0,0 +1,16 @@
1
+ partweave
2
+ Copyright 2026 Agrim Sigdel
3
+
4
+ Created and maintained by Agrim Sigdel <sigdelagrim35@gmail.com>.
5
+ Source: https://github.com/Agrim-Sigdel/partweave
6
+
7
+ This product is licensed under the Apache License, Version 2.0 (see LICENSE).
8
+
9
+ "partweave" is a trademark of Agrim Sigdel. The Apache License grants no
10
+ trademark rights (see Section 6): forks and derivative works must not use the
11
+ "partweave" name, logo, or branding to identify themselves or to market a
12
+ derived product. You may of course state, factually, that your work is derived
13
+ from or built on partweave.
14
+
15
+ Per Section 4(d) of the Apache License, this NOTICE (and the attribution above)
16
+ must be reproduced in any redistribution or derivative work.
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # partweave — a modular full-stack scaffolder
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/partweave.svg)](https://www.npmjs.com/package/partweave)
4
+ [![CI](https://github.com/Agrim-Sigdel/partweave/actions/workflows/ci.yml/badge.svg)](https://github.com/Agrim-Sigdel/partweave/actions/workflows/ci.yml)
5
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
6
+ [![Node](https://img.shields.io/node/v/partweave.svg)](https://www.npmjs.com/package/partweave)
7
+
3
8
  Generate new projects that contain **only the parts you pick** — a Django server, a
4
9
  Next.js web app, an Expo mobile app, and cross-cutting components (auth, storage, docker,
5
10
  CI) — all wired together.
@@ -113,6 +118,19 @@ pnpm --filter partweave typecheck # typecheck the engine
113
118
  The generator locates the catalog by walking up from the CLI to find `modules/`.
114
119
  Override with `PARTWEAVE_MODULES_DIR=/path/to/modules`.
115
120
 
121
+ See **[CONTRIBUTING.md](CONTRIBUTING.md)** for the full setup, project layout, the checks CI
122
+ enforces, and how to add a component.
123
+
124
+ ## Contributing
125
+
126
+ Contributions are welcome! The highest-leverage change is usually **a new component** — a
127
+ folder under `modules/` with a `module.json`, no engine changes required.
128
+
129
+ - Read [CONTRIBUTING.md](CONTRIBUTING.md) and the [module contract](docs/module-contract.md).
130
+ - Be kind — this project follows a [Code of Conduct](CODE_OF_CONDUCT.md).
131
+ - Found a security issue? See [SECURITY.md](SECURITY.md) — please report it privately.
132
+ - Release notes live in [CHANGELOG.md](CHANGELOG.md).
133
+
116
134
  ## Releasing
117
135
 
118
136
  The CLI is published to npm as [`partweave`](https://www.npmjs.com/package/partweave).
@@ -124,6 +142,23 @@ npm version patch --workspace partweave # bump version + create a matching git
124
142
  git push --follow-tags # push the tag → CI publishes to npm
125
143
  ```
126
144
 
127
- > One-time setup: add an `NPM_TOKEN` (an npm "Automation" access token) to the repo's
128
- > **Settings Secrets and variables Actions**. The `prepack` script copies `modules/` +
129
- > README + LICENSE into the package, so `npm publish` ships a self-contained CLI.
145
+ > The `prepack` script copies `modules/`, README, LICENSE + NOTICE into the package, so
146
+ > `npm publish` ships a self-contained CLI. Auth is via npm Trusted Publishing (OIDC) —
147
+ > no `NPM_TOKEN` secret is needed (see [`publish.yml`](.github/workflows/publish.yml)).
148
+
149
+ ## Author
150
+
151
+ Created and maintained by **Agrim Sigdel** — <sigdelagrim35@gmail.com>.
152
+
153
+ If you build something with partweave, a mention or a link back is always appreciated.
154
+
155
+ ## License
156
+
157
+ Licensed under the [Apache License 2.0](LICENSE) — permissive and free for commercial use.
158
+
159
+ - **Projects you generate are yours.** partweave copies template code into your project; you
160
+ own that output and carry no obligation back to this repo.
161
+ - **Attribution (Apache §4d):** redistributing partweave itself — or a fork of it — must keep
162
+ the [`LICENSE`](LICENSE) and [`NOTICE`](NOTICE) files intact.
163
+ - **Trademark (Apache §6):** the **partweave** name and branding are reserved. You may fork
164
+ and even sell the code, but not market a derivative under the "partweave" name.
@@ -4,8 +4,80 @@
4
4
  import { spawnSync as spawnSync3 } from "child_process";
5
5
  import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
6
6
  import { basename, resolve as resolve3 } from "path";
7
- import { confirm as confirm3, intro, isCancel as isCancel3, log as log2, note, outro, spinner as spinner2 } from "@clack/prompts";
8
- import pc2 from "picocolors";
7
+ import { confirm as confirm3, intro, isCancel as isCancel3, log as log2, note as note2, outro, spinner as spinner2 } from "@clack/prompts";
8
+ import pc3 from "picocolors";
9
+
10
+ // src/banner.ts
11
+ import pc from "picocolors";
12
+ var WEAVE = ["\u2588", "\u2593"];
13
+ var weaveCell = (row, col) => WEAVE[(row + col) % 2];
14
+ var GLYPHS = {
15
+ p: ["\u2588\u2588\u2588 ", "\u2588 \u2588", "\u2588\u2588\u2588 ", "\u2588 ", "\u2588 "],
16
+ a: [" \u2588\u2588 ", "\u2588 \u2588", "\u2588\u2588\u2588\u2588", "\u2588 \u2588", "\u2588 \u2588"],
17
+ r: ["\u2588\u2588\u2588 ", "\u2588 \u2588", "\u2588\u2588\u2588 ", "\u2588 \u2588 ", "\u2588 \u2588"],
18
+ t: ["\u2588\u2588\u2588\u2588\u2588", " \u2588 ", " \u2588 ", " \u2588 ", " \u2588 "],
19
+ w: ["\u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588 \u2588", "\u2588\u2588 \u2588\u2588", "\u2588 \u2588"],
20
+ e: ["\u2588\u2588\u2588\u2588", "\u2588 ", "\u2588\u2588\u2588 ", "\u2588 ", "\u2588\u2588\u2588\u2588"],
21
+ v: ["\u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588", " \u2588 \u2588 ", " \u2588 "]
22
+ };
23
+ var WORD = "partweave";
24
+ var HEIGHT = 5;
25
+ function colorMode() {
26
+ if (process.env.NO_COLOR || !process.stdout.isTTY) return "none";
27
+ if (/truecolor|24bit/i.test(process.env.COLORTERM ?? "")) return "truecolor";
28
+ return "basic";
29
+ }
30
+ var STOPS = [
31
+ [124, 92, 246],
32
+ // violet
33
+ [190, 75, 219],
34
+ // magenta
35
+ [232, 74, 143],
36
+ // pink
37
+ [225, 51, 74],
38
+ // red
39
+ [248, 96, 76]
40
+ // coral / bright red
41
+ ];
42
+ function gradient(t) {
43
+ const n = STOPS.length - 1;
44
+ const scaled = Math.min(Math.max(t, 0), 1) * n;
45
+ const i = Math.min(Math.floor(scaled), n - 1);
46
+ const local = scaled - i;
47
+ const [a, b] = [STOPS[i], STOPS[i + 1]];
48
+ return [0, 1, 2].map((k) => Math.round(a[k] + (b[k] - a[k]) * local));
49
+ }
50
+ function paint(ch, t, mode) {
51
+ if (ch === " ") return " ";
52
+ if (mode === "none") return ch;
53
+ if (mode === "basic") return t < 0.5 ? pc.magentaBright(ch) : pc.redBright(ch);
54
+ const [r, g, b] = gradient(t);
55
+ return `\x1B[38;2;${r};${g};${b}m${ch}\x1B[39m`;
56
+ }
57
+ function compact(mode) {
58
+ return " " + (mode === "none" ? "partweave" : word2tone("partweave", mode));
59
+ }
60
+ function word2tone(word, mode) {
61
+ if (mode === "none") return word;
62
+ return [...word].map((ch, i) => paint(ch, word.length > 1 ? i / (word.length - 1) : 0, mode)).join("");
63
+ }
64
+ function renderBanner() {
65
+ const mode = colorMode();
66
+ if (!process.stdout.isTTY) return compact(mode);
67
+ const cols = process.stdout.columns ?? 80;
68
+ const rows = [];
69
+ for (let r = 0; r < HEIGHT; r++) {
70
+ rows.push([...WORD].map((ch) => GLYPHS[ch]?.[r] ?? "").join(" "));
71
+ }
72
+ const width = rows[0].length;
73
+ if (width + 2 > cols) return compact(mode);
74
+ const painted = rows.map(
75
+ (row, r) => " " + [...row].map(
76
+ (ch, c) => ch === " " ? " " : paint(weaveCell(r, c), width > 1 ? c / (width - 1) : 0, mode)
77
+ ).join("")
78
+ );
79
+ return painted.join("\n");
80
+ }
9
81
 
10
82
  // src/compose.ts
11
83
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
@@ -26,14 +98,19 @@ function injectAtAnchor(content, anchorId, lines) {
26
98
  throw new Error(`Anchor <partweave:${anchorId}> not found in target file`);
27
99
  }
28
100
  const indent = match[1] ?? "";
29
- const existing = new Set(
30
- content.split("\n").map((l) => l.trim()).filter(Boolean)
31
- );
32
- const toInsert = lines.filter((l) => !existing.has(l.trim()));
101
+ const before = content.slice(0, match.index).split("\n");
102
+ if (before[before.length - 1] === "") before.pop();
103
+ const block = /* @__PURE__ */ new Set();
104
+ for (let i = before.length - 1; i >= 0; i--) {
105
+ const trimmed = before[i].trim();
106
+ if (trimmed === "") break;
107
+ block.add(trimmed);
108
+ }
109
+ const toInsert = lines.filter((l) => !block.has(l.trim()));
33
110
  if (toInsert.length === 0) return { content, inserted: 0 };
34
- const block = toInsert.map((l) => l ? indent + l : "").join("\n") + "\n";
111
+ const insertion = toInsert.map((l) => l ? indent + l : "").join("\n") + "\n";
35
112
  const anchorStart = match.index;
36
- const newContent = content.slice(0, anchorStart) + block + content.slice(anchorStart);
113
+ const newContent = content.slice(0, anchorStart) + insertion + content.slice(anchorStart);
37
114
  return { content: newContent, inserted: toInsert.length };
38
115
  }
39
116
  function parseDep(dep) {
@@ -50,34 +127,35 @@ function normalizeWorkspaceDeps(deps, workspaceRange) {
50
127
  return dep;
51
128
  });
52
129
  }
130
+ function higherVersion(a, b) {
131
+ const core = (v) => {
132
+ const m = v.match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
133
+ if (!m || m[1] === void 0) return null;
134
+ return [m[1], m[2], m[3]].map((n) => n === void 0 ? 0 : Number(n));
135
+ };
136
+ const ca = core(a);
137
+ const cb = core(b);
138
+ if (!ca || !cb) return a;
139
+ for (let i = 0; i < 3; i++) {
140
+ if (ca[i] !== cb[i]) return ca[i] > cb[i] ? a : b;
141
+ }
142
+ return a;
143
+ }
53
144
  function mergePackageJsonDeps(pkgJson, deps, field = "dependencies") {
54
145
  if (deps.length === 0) return pkgJson;
55
146
  const pkg = JSON.parse(pkgJson);
56
147
  const bucket = pkg[field] ?? {};
57
148
  for (const dep of deps) {
58
149
  const { name, version } = parseDep(dep);
59
- if (!(name in bucket)) bucket[name] = version;
150
+ bucket[name] = name in bucket ? higherVersion(bucket[name], version) : version;
60
151
  }
61
152
  pkg[field] = Object.fromEntries(
62
153
  Object.entries(bucket).sort(([a], [b]) => a.localeCompare(b))
63
154
  );
64
155
  return JSON.stringify(pkg, null, 2) + "\n";
65
156
  }
66
- function appendEnv(envBody, entries, heading) {
67
- const keys = Object.keys(entries);
68
- if (keys.length === 0) return envBody;
69
- const present = new Set(
70
- envBody.split("\n").map((l) => l.split("=")[0]?.trim()).filter(Boolean)
71
- );
72
- const missing = keys.filter((k) => !present.has(k));
73
- if (missing.length === 0) return envBody;
74
- let out = envBody.replace(/\n*$/, "\n");
75
- if (heading) out += `
76
- # ${heading}
77
- `;
78
- for (const k of missing) out += `${k}=${entries[k]}
79
- `;
80
- return out;
157
+ function pyDepName(dep) {
158
+ return dep.trim().replace(/^["']|["']$/g, "").split(/[<>=!~;[\s]/)[0].trim().toLowerCase();
81
159
  }
82
160
 
83
161
  // src/fsutil.ts
@@ -188,7 +266,7 @@ function jsPmProfile(name) {
188
266
  runAll: (script) => `npm run ${script} --workspaces --if-present`,
189
267
  packageManagerField: null,
190
268
  usesWorkspaceYaml: false,
191
- needsHoistNpmrc: false,
269
+ needsHoisting: false,
192
270
  workspaceRange: "*"
193
271
  };
194
272
  }
@@ -199,7 +277,7 @@ function jsPmProfile(name) {
199
277
  runAll: (script) => `pnpm -r ${script}`,
200
278
  packageManagerField: PNPM_VERSION,
201
279
  usesWorkspaceYaml: true,
202
- needsHoistNpmrc: true,
280
+ needsHoisting: true,
203
281
  workspaceRange: "workspace:*"
204
282
  };
205
283
  }
@@ -277,7 +355,11 @@ function buildJsWorkspace(ctx) {
277
355
  if (!jsPmProfile(ctx.jsPm).usesWorkspaceYaml) return null;
278
356
  const members = jsMembers(ctx);
279
357
  if (members.length === 0) return null;
280
- return "packages:\n" + members.map((m) => ` - "${m}"`).join("\n") + "\n";
358
+ let out = "packages:\n" + members.map((m) => ` - "${m}"`).join("\n") + "\n";
359
+ if (ctx.hasMobile && jsPmProfile(ctx.jsPm).needsHoisting) {
360
+ out += "\n# Expo / React Native need a flat node_modules layout.\nnodeLinker: hoisted\nshamefullyHoist: true\n";
361
+ }
362
+ return out;
281
363
  }
282
364
  function buildRootPackageJson(ctx, hasDocker) {
283
365
  const anyJs = ctx.hasWeb || ctx.hasMobile || ctx.hasShared;
@@ -307,6 +389,25 @@ function buildRootPackageJson(ctx, hasDocker) {
307
389
  if (anyJs && !pm.usesWorkspaceYaml) pkg.workspaces = jsMembers(ctx);
308
390
  pkg.scripts = scripts;
309
391
  if (anyJs) pkg.devDependencies = { typescript: "^5.7.2" };
392
+ if (anyJs && ctx.jsPm === "pnpm") {
393
+ const allowed = {};
394
+ if (ctx.hasMobile) {
395
+ Object.assign(allowed, {
396
+ abab: "2",
397
+ domexception: "4",
398
+ glob: "7",
399
+ inflight: "1",
400
+ rimraf: "3",
401
+ uuid: "7"
402
+ });
403
+ }
404
+ if (ctx.hasWeb || ctx.hasMobile) {
405
+ allowed["whatwg-encoding"] = ctx.hasWeb && ctx.hasMobile ? "2 || 3" : ctx.hasWeb ? "3" : "2";
406
+ }
407
+ if (Object.keys(allowed).length > 0) {
408
+ pkg.pnpm = { allowedDeprecatedVersions: allowed };
409
+ }
410
+ }
310
411
  return JSON.stringify(pkg, null, 2) + "\n";
311
412
  }
312
413
  function buildTaskRunner(ctx, hasDocker) {
@@ -525,55 +626,82 @@ function buildTsconfigBase(ctx) {
525
626
  2
526
627
  ) + "\n";
527
628
  }
528
- function buildNpmrc(ctx) {
529
- if (!ctx.hasMobile) return null;
530
- if (!jsPmProfile(ctx.jsPm).needsHoistNpmrc) return null;
531
- return [
532
- "# Expo / React Native need a flat node_modules layout.",
533
- "node-linker=hoisted",
534
- "shamefully-hoist=true",
535
- ""
536
- ].join("\n");
537
- }
538
- function buildBaseEnv(ctx) {
539
- const lines = [
540
- "# Environment for " + ctx.projectName,
541
- "#",
542
- "# Every value the app reads from the environment lives here. Copy this file to",
543
- "# `.env` and adjust as needed \u2014 the server, web/mobile apps, and each component",
544
- "# read their configuration from it, so there's nothing to configure elsewhere.",
545
- ""
546
- ];
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 = [];
547
649
  if (ctx.hasServer) {
548
- lines.push(
549
- "# --- server ---",
550
- "# Unique per-project key generated at scaffold time; signs sessions and JWTs.",
551
- "# Use a separate secret value in production (and never commit the real one).",
552
- `DJANGO_SECRET_KEY=${randomBytes(48).toString("base64url")}`,
650
+ const serverBase = (secretLine) => [
651
+ `# ${ctx.projectName} \u2014 server (Django). Read by apps/server.`,
652
+ "# `.env` is gitignored; keep real secrets here, not in `.env.example`.",
653
+ "",
654
+ "# Unique per-project key; signs sessions and JWTs.",
655
+ secretLine,
553
656
  "DJANGO_DEBUG=true",
554
- "DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1",
555
- "# Web origins allowed to call the API when DEBUG is off (comma-separated).",
556
- "# In DEBUG every origin is allowed, so this is only needed in production.",
657
+ "# Leave DJANGO_ALLOWED_HOSTS unset in dev: with DEBUG on, any host is allowed,",
658
+ "# so a phone/simulator can reach the server over your LAN (e.g. 192.168.x.y).",
659
+ "# Set it (comma-separated) in production, e.g. DJANGO_ALLOWED_HOSTS=api.example.com",
660
+ "# Origins allowed to call the API when DEBUG is off (comma-separated).",
557
661
  "# DJANGO_CORS_ALLOWED_ORIGINS=https://app.example.com",
558
- "# DATABASE_URL \u2014 the server uses local SQLite by default; set a database URL",
559
- "# 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.",
560
663
  ""
561
- );
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
+ });
562
670
  }
563
671
  if (ctx.hasWeb) {
564
- lines.push("# --- web ---", "NEXT_PUBLIC_API_URL=http://localhost:8000", "");
672
+ const web = [
673
+ `# ${ctx.projectName} \u2014 web (Next.js). Read by apps/web.`,
674
+ "# Only NEXT_PUBLIC_* is exposed to the browser.",
675
+ "NEXT_PUBLIC_API_URL=http://localhost:8000",
676
+ "",
677
+ ...componentBlocks.web
678
+ ];
679
+ const body = render2(web);
680
+ files.push({ dir: "apps/web", example: body, env: body });
565
681
  }
566
682
  if (ctx.hasMobile) {
567
- lines.push(
568
- "# --- mobile (Expo) ---",
569
- "# In dev the app auto-detects your machine's LAN IP so a physical device can",
570
- "# reach the server \u2014 leave this unset. Set it for simulators or production,",
571
- "# 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.",
572
687
  "# EXPO_PUBLIC_API_URL=http://localhost:8000",
573
- ""
574
- );
575
- }
576
- return lines.join("\n");
688
+ "",
689
+ ...componentBlocks.mobile
690
+ ];
691
+ const body = render2(mobile);
692
+ files.push({ dir: "apps/mobile", example: body, env: body });
693
+ }
694
+ if (componentBlocks.root.length > 0) {
695
+ const root = [
696
+ `# ${ctx.projectName} \u2014 infrastructure. Read by docker compose (--env-file .env).`,
697
+ "# Keep these in sync with DATABASE_URL in apps/server/.env.",
698
+ "",
699
+ ...componentBlocks.root
700
+ ];
701
+ const body = render2(root);
702
+ files.push({ dir: "", example: body, env: body });
703
+ }
704
+ return files;
577
705
  }
578
706
  function jsCiSteps(ctx) {
579
707
  if (ctx.jsPm === "npm") {
@@ -665,7 +793,10 @@ ${jsCiSteps(ctx)}
665
793
  function buildReadme(ctx, modules) {
666
794
  const parts = [];
667
795
  parts.push(`# ${ctx.projectName}`, "", ctx.description, "");
668
- parts.push("Generated with **partweave** \u2014 a modular full-stack scaffolder.", "");
796
+ parts.push(
797
+ "Generated with [partweave](https://github.com/Agrim-Sigdel/partweave) \u2014 a modular full-stack scaffolder by Agrim Sigdel.",
798
+ ""
799
+ );
669
800
  parts.push("## What's inside", "");
670
801
  if (ctx.hasServer) parts.push(`- \`apps/server\` \u2014 Django + DRF API (managed by \`${ctx.pyPm}\`)`);
671
802
  if (ctx.hasWeb) parts.push("- `apps/web` \u2014 Next.js web app");
@@ -695,7 +826,18 @@ function buildReadme(ctx, modules) {
695
826
  if (ctx.hasWeb) parts.push("npm run web # http://localhost:3000");
696
827
  if (ctx.hasMobile) parts.push("npm run mobile # Expo dev server");
697
828
  parts.push("```", "");
698
- parts.push("Copy `.env.example` to `.env` and fill in values before running.", "");
829
+ parts.push("## Configuration", "");
830
+ parts.push(
831
+ "Each app reads its **own** env file, created for you (and gitignored) with a",
832
+ "committed `.env.example` template alongside it. Edit the `.env` to change values:",
833
+ ""
834
+ );
835
+ const envFiles = [];
836
+ if (ctx.hasServer) envFiles.push("- `apps/server/.env` \u2014 Django secret key, allowed hosts, `DATABASE_URL`");
837
+ if (ctx.hasWeb) envFiles.push("- `apps/web/.env` \u2014 `NEXT_PUBLIC_*` (browser-exposed)");
838
+ if (ctx.hasMobile) envFiles.push("- `apps/mobile/.env` \u2014 `EXPO_PUBLIC_*` (app-exposed)");
839
+ if (hasDocker) envFiles.push("- `.env` (root) \u2014 `POSTGRES_*` for the database container");
840
+ parts.push(...envFiles, "");
699
841
  return parts.join("\n");
700
842
  }
701
843
  function buildServerDockerfile(ctx) {
@@ -822,7 +964,7 @@ var ManifestSchema = z.object({
822
964
  conflicts: z.array(z.string()).default([]),
823
965
  /** capability/interface this module satisfies (for conflict grouping) */
824
966
  provides: z.string().optional(),
825
- /** env keys → default value, appended to .env.example */
967
+ /** env keys → default value; routed to the consuming app's .env/.env.example by prefix (POSTGRES_→root, NEXT_PUBLIC_→web, EXPO_PUBLIC_→mobile, else server) */
826
968
  env: z.record(z.string()).default({}),
827
969
  /** per-target wiring (files are copied separately from wiring injection) */
828
970
  wiring: z.record(z.enum(TARGETS), WiringForTargetSchema).default({}),
@@ -912,6 +1054,25 @@ function injectIntoFiles(index, anchorId, lines, targetLabel) {
912
1054
  writeFileSync2(file, next);
913
1055
  }
914
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
+ }
915
1076
  function applyWiring(outDir, targets, modules, workspaceRange) {
916
1077
  const indexes = /* @__PURE__ */ new Map();
917
1078
  for (const t of targets) {
@@ -929,8 +1090,7 @@ function applyWiring(outDir, targets, modules, workspaceRange) {
929
1090
  }
930
1091
  if (wiring.deps?.length) {
931
1092
  if (t === "server") {
932
- const lines = wiring.deps.map((d) => `"${d}",`);
933
- injectIntoFiles(index, "deps", lines, t);
1093
+ injectPyDeps(index, wiring.deps, t);
934
1094
  } else {
935
1095
  const pkgPath = join2(targetDir, "package.json");
936
1096
  if (existsSync2(pkgPath)) {
@@ -945,14 +1105,19 @@ function applyWiring(outDir, targets, modules, workspaceRange) {
945
1105
  }
946
1106
  }
947
1107
  }
948
- function applyEnv(outDir, modules) {
949
- const envPath = join2(outDir, ".env.example");
950
- let body = existsSync2(envPath) ? readFileSync2(envPath, "utf8") : "";
951
- for (const mod of modules) {
952
- if (Object.keys(mod.manifest.env).length === 0) continue;
953
- body = appendEnv(body, mod.manifest.env, mod.manifest.title);
1108
+ function writeEnvFiles(outDir, ctx, modules) {
1109
+ const written = [];
1110
+ for (const file of buildEnvFiles(ctx, modules)) {
1111
+ const exampleRel = join2(file.dir, ".env.example");
1112
+ writeFileEnsured(join2(outDir, exampleRel), file.example);
1113
+ written.push(exampleRel);
1114
+ const envRel = join2(file.dir, ".env");
1115
+ if (!existsSync2(join2(outDir, envRel))) {
1116
+ writeFileEnsured(join2(outDir, envRel), file.env);
1117
+ written.push(envRel);
1118
+ }
954
1119
  }
955
- if (body) writeFileSync2(envPath, body);
1120
+ return written;
956
1121
  }
957
1122
  function writeStructuralRootFiles(outDir, ctx, modules) {
958
1123
  const workspace = buildJsWorkspace(ctx);
@@ -964,13 +1129,10 @@ function writeStructuralRootFiles(outDir, ctx, modules) {
964
1129
  if (turbo) writeFileEnsured(join2(outDir, "turbo.json"), turbo);
965
1130
  const tsbase = buildTsconfigBase(ctx);
966
1131
  if (tsbase) writeFileEnsured(join2(outDir, "tsconfig.base.json"), tsbase);
967
- const npmrc = buildNpmrc(ctx);
968
- if (npmrc) writeFileEnsured(join2(outDir, ".npmrc"), npmrc);
969
1132
  const pipSync = buildPipSyncScript(ctx);
970
1133
  if (pipSync) writeFileEnsured(join2(outDir, "apps/server/scripts/sync_deps.py"), pipSync);
971
1134
  writeFileEnsured(join2(outDir, "scripts/run.mjs"), buildTaskRunner(ctx, hasDocker));
972
1135
  writeFileEnsured(join2(outDir, "Makefile"), buildMakefile(ctx, hasDocker));
973
- writeFileEnsured(join2(outDir, ".env.example"), buildBaseEnv(ctx));
974
1136
  }
975
1137
  function compose(opts) {
976
1138
  const { selection, registry, scaffoldTargets, wireTargets, rootFiles } = opts;
@@ -999,7 +1161,7 @@ function compose(opts) {
999
1161
  }
1000
1162
  }
1001
1163
  applyWiring(outDir, wireTargets, modules, jsPmProfile(ctx.jsPm).workspaceRange);
1002
- applyEnv(outDir, modules);
1164
+ if (rootFiles !== "none") written.push(...writeEnvFiles(outDir, ctx, modules));
1003
1165
  if (selection.modules.includes("ci")) {
1004
1166
  for (const [rel, content] of Object.entries(buildCiWorkflows(ctx))) {
1005
1167
  writeFileEnsured(join2(outDir, rel), content);
@@ -1195,11 +1357,28 @@ function resolveModules(registry, chosen) {
1195
1357
  provided.set(cap, id);
1196
1358
  }
1197
1359
  }
1198
- const order = [...resolved].sort((a, b) => {
1199
- const da = registry.require(a).manifest.requires.length;
1200
- const db = registry.require(b).manifest.requires.length;
1201
- return da - db || a.localeCompare(b);
1202
- });
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
+ }
1203
1382
  return { modules: order, autoAdded: [...autoAdded].sort() };
1204
1383
  }
1205
1384
  function validateApps(registry, moduleIds, apps) {
@@ -1228,12 +1407,13 @@ import {
1228
1407
  confirm as confirm2,
1229
1408
  isCancel as isCancel2,
1230
1409
  multiselect,
1410
+ note,
1231
1411
  select,
1232
1412
  text
1233
1413
  } from "@clack/prompts";
1234
- import pc from "picocolors";
1414
+ import pc2 from "picocolors";
1235
1415
  import { resolve as resolve2 } from "path";
1236
- var MULTI_HINT = pc.dim("\u2191/\u2193 move \xB7 space toggle \xB7 a all \xB7 enter confirm");
1416
+ var MULTI_HINT = pc2.dim("\u2191/\u2193 move \xB7 space toggle \xB7 a all \xB7 enter confirm");
1237
1417
  function bail(v) {
1238
1418
  if (isCancel2(v)) {
1239
1419
  cancel("Cancelled.");
@@ -1313,9 +1493,19 @@ async function promptCreate(registry, defaults) {
1313
1493
  bail(pyRaw);
1314
1494
  pyPm = pyRaw;
1315
1495
  }
1316
- const ok = await confirm2({
1317
- message: `Scaffold ${apps.join(" + ")}${modules.length ? " with " + modules.join(", ") : ""} into ${outDir}?`
1318
- });
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?" });
1319
1509
  bail(ok);
1320
1510
  if (!ok) {
1321
1511
  cancel("Cancelled.");
@@ -1325,6 +1515,21 @@ async function promptCreate(registry, defaults) {
1325
1515
  }
1326
1516
 
1327
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
+ }
1328
1533
  function resolvePm(value, allowed, detect, flag) {
1329
1534
  if (value === void 0) return detect();
1330
1535
  if (allowed.includes(value)) return value;
@@ -1346,7 +1551,8 @@ function defaultModules(registry, apps) {
1346
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);
1347
1552
  }
1348
1553
  async function runCreate(flags) {
1349
- intro(pc2.bgCyan(pc2.black(" partweave ")) + pc2.dim(" full-stack scaffolder"));
1554
+ console.log("\n" + renderBanner());
1555
+ intro(pc3.dim("full-stack scaffolder \u2014 pick the parts, own the code"));
1350
1556
  const registry = new Registry();
1351
1557
  const flagApps = appsFromFlags(flags);
1352
1558
  const nonInteractive = flags.yes === true || flagApps !== null;
@@ -1447,6 +1653,27 @@ async function runCreate(flags) {
1447
1653
  installed = false;
1448
1654
  }
1449
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
+ }
1450
1677
  const rel = basename(choices.outDir);
1451
1678
  const hasDocker = resolved.modules.includes("docker");
1452
1679
  const steps = [`cd ${rel}`];
@@ -1457,12 +1684,12 @@ async function runCreate(flags) {
1457
1684
  }
1458
1685
  if (choices.apps.includes("web")) steps.push("npm run web");
1459
1686
  if (choices.apps.includes("mobile")) steps.push("npm run mobile");
1460
- note(
1461
- steps.join("\n") + "\n\n" + pc2.dim("These work on macOS, Linux & Windows. On macOS/Linux, `make <task>` works too."),
1687
+ note2(
1688
+ steps.join("\n") + "\n\n" + pc3.dim("These work on macOS, Linux & Windows. On macOS/Linux, `make <task>` works too."),
1462
1689
  "Next steps"
1463
1690
  );
1464
- if (result.notes.length) note(result.notes.join("\n"), "Notes");
1465
- outro(pc2.green("Done."));
1691
+ if (result.notes.length) note2(result.notes.join("\n"), "Notes");
1692
+ outro(pc3.green("Done."));
1466
1693
  }
1467
1694
 
1468
1695
  export {
@@ -1482,4 +1709,4 @@ export {
1482
1709
  validateApps,
1483
1710
  runCreate
1484
1711
  };
1485
- //# sourceMappingURL=chunk-XB7H4V4J.js.map
1712
+ //# sourceMappingURL=chunk-Y5FZVVV6.js.map
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCreate
4
- } from "./chunk-XB7H4V4J.js";
4
+ } from "./chunk-Y5FZVVV6.js";
5
5
 
6
6
  // src/create-bin.ts
7
7
  import { Command } from "commander";
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  selectedTargets,
16
16
  validateApps,
17
17
  writeProjectManifest
18
- } from "./chunk-XB7H4V4J.js";
18
+ } from "./chunk-Y5FZVVV6.js";
19
19
 
20
20
  // src/index.ts
21
21
  import { Command } from "commander";
@@ -178,7 +178,7 @@ async function runDoctor(flags) {
178
178
  function buildProgram() {
179
179
  const program = new Command();
180
180
  program.name("partweave").description("A modular full-stack scaffolder \u2014 pick parts, generate only that code.").version("0.1.0");
181
- program.command("create", { isDefault: true }).argument("[name]", "project name").description("Scaffold a new project").option("-d, --dir <path>", "target directory").option("--server", "include the Django server").option("--no-server", "exclude the server").option("--web", "include the Next.js web app").option("--no-web", "exclude the web app").option("--mobile", "include the Expo mobile app").option("--no-mobile", "exclude the mobile app").option("--with <ids>", "comma-separated component ids (e.g. auth,docker,ci)").option("--js-pm <pm>", "JS package manager: pnpm | npm (default: auto-detect)").option("--py-pm <pm>", "Python package manager: uv | pip (default: auto-detect)").option("-y, --yes", "skip prompts; use flags/defaults").option("-f, --force", "write into a non-empty directory").option("--install", "install dependencies after scaffolding (default: ask, off with --yes)").action(async (name, opts) => {
181
+ program.command("create", { isDefault: true }).argument("[name]", "project name").description("Scaffold a new project").option("-d, --dir <path>", "target directory").option("--server", "include the Django server").option("--no-server", "exclude the server").option("--web", "include the Next.js web app").option("--no-web", "exclude the web app").option("--mobile", "include the Expo mobile app").option("--no-mobile", "exclude the mobile app").option("--with <ids>", "comma-separated component ids (e.g. auth,docker,ci)").option("--js-pm <pm>", "JS package manager: pnpm | npm (default: auto-detect)").option("--py-pm <pm>", "Python package manager: uv | pip (default: auto-detect)").option("-y, --yes", "skip prompts; use flags/defaults").option("-f, --force", "write into a non-empty directory").option("--install", "install dependencies after scaffolding (default: ask, off with --yes)").option("--git", "initialize a git repo + initial commit (default: ask, off with --yes)").option("--no-git", "skip git initialization").action(async (name, opts) => {
182
182
  const flags = { ...opts, name: name ?? opts.name };
183
183
  await runCreate(flags);
184
184
  });
@@ -17,4 +17,20 @@ config.resolver.nodeModulesPaths = [
17
17
  // non-hoisted install) some Expo sub-deps (e.g. expo-asset) resolve from
18
18
  // expo/node_modules, and disabling the walk-up breaks bundling them.
19
19
 
20
+ // Keep test files out of the native bundle. Expo Router builds a require.context
21
+ // over the whole app/ directory, so a colocated `*.test.tsx` would otherwise get
22
+ // bundled — dragging in @testing-library/react-native and Node built-ins
23
+ // (console, util) that don't exist in the RN runtime, and failing the build.
24
+ // Metro's blockList removes them from resolution (and thus from require.context);
25
+ // Jest doesn't read this config, so `npm test` still finds and runs them.
26
+ config.resolver.blockList = [
27
+ ...(Array.isArray(config.resolver.blockList)
28
+ ? config.resolver.blockList
29
+ : config.resolver.blockList
30
+ ? [config.resolver.blockList]
31
+ : []),
32
+ /\.(test|spec)\.[jt]sx?$/,
33
+ /[/\\]__tests__[/\\]/,
34
+ ];
35
+
20
36
  module.exports = config;
@@ -16,7 +16,7 @@
16
16
  }
17
17
  },
18
18
  "notes": [
19
- "PostgreSQL: set DATABASE_URL in .env (already added to .env.example).",
19
+ "PostgreSQL: DATABASE_URL lives in apps/server/.env (created for you, with a matching .env.example).",
20
20
  "Need a local database? Include the 'docker' component and run `make db-up`."
21
21
  ]
22
22
  }
@@ -14,6 +14,6 @@
14
14
  },
15
15
  "notes": [
16
16
  "Local DB: `make db-up` (docker compose). Server image: `docker build apps/server`.",
17
- "Postgres creds/port come from .env (POSTGRES_USER/PASSWORD/DB/PORT); keep them in sync with DATABASE_URL."
17
+ "Postgres creds/port come from the root `.env` (POSTGRES_USER/PASSWORD/DB/PORT); keep them in sync with DATABASE_URL in apps/server/.env."
18
18
  ]
19
19
  }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "partweave",
3
- "version": "0.3.0",
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
- "license": "MIT",
6
+ "license": "Apache-2.0",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
10
- "author": "Agrim Sigdel",
10
+ "author": "Agrim Sigdel <sigdelagrim35@gmail.com>",
11
11
  "homepage": "https://github.com/Agrim-Sigdel/partweave#readme",
12
12
  "repository": {
13
13
  "type": "git",
@@ -41,7 +41,8 @@
41
41
  "dist/**/*.js",
42
42
  "modules",
43
43
  "README.md",
44
- "LICENSE"
44
+ "LICENSE",
45
+ "NOTICE"
45
46
  ],
46
47
  "scripts": {
47
48
  "build": "tsup",