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 +201 -21
- package/NOTICE +16 -0
- package/README.md +38 -3
- package/dist/{chunk-XB7H4V4J.js → chunk-Y5FZVVV6.js} +326 -99
- package/dist/create-bin.js +1 -1
- package/dist/index.js +2 -2
- package/modules/_core/mobile/metro.config.js +16 -0
- package/modules/db-postgres/module.json +1 -1
- package/modules/docker/module.json +1 -1
- package/package.json +5 -4
package/LICENSE
CHANGED
|
@@ -1,21 +1,201 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/partweave)
|
|
4
|
+
[](https://github.com/Agrim-Sigdel/partweave/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](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
|
-
>
|
|
128
|
-
>
|
|
129
|
-
>
|
|
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
|
|
7
|
+
import { confirm as confirm3, intro, isCancel as isCancel3, log as log2, note as note2, outro, spinner as spinner2 } from "@clack/prompts";
|
|
8
|
+
import pc3 from "picocolors";
|
|
9
|
+
|
|
10
|
+
// src/banner.ts
|
|
11
|
+
import pc from "picocolors";
|
|
12
|
+
var WEAVE = ["\u2588", "\u2593"];
|
|
13
|
+
var weaveCell = (row, col) => WEAVE[(row + col) % 2];
|
|
14
|
+
var GLYPHS = {
|
|
15
|
+
p: ["\u2588\u2588\u2588 ", "\u2588 \u2588", "\u2588\u2588\u2588 ", "\u2588 ", "\u2588 "],
|
|
16
|
+
a: [" \u2588\u2588 ", "\u2588 \u2588", "\u2588\u2588\u2588\u2588", "\u2588 \u2588", "\u2588 \u2588"],
|
|
17
|
+
r: ["\u2588\u2588\u2588 ", "\u2588 \u2588", "\u2588\u2588\u2588 ", "\u2588 \u2588 ", "\u2588 \u2588"],
|
|
18
|
+
t: ["\u2588\u2588\u2588\u2588\u2588", " \u2588 ", " \u2588 ", " \u2588 ", " \u2588 "],
|
|
19
|
+
w: ["\u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588 \u2588", "\u2588\u2588 \u2588\u2588", "\u2588 \u2588"],
|
|
20
|
+
e: ["\u2588\u2588\u2588\u2588", "\u2588 ", "\u2588\u2588\u2588 ", "\u2588 ", "\u2588\u2588\u2588\u2588"],
|
|
21
|
+
v: ["\u2588 \u2588", "\u2588 \u2588", "\u2588 \u2588", " \u2588 \u2588 ", " \u2588 "]
|
|
22
|
+
};
|
|
23
|
+
var WORD = "partweave";
|
|
24
|
+
var HEIGHT = 5;
|
|
25
|
+
function colorMode() {
|
|
26
|
+
if (process.env.NO_COLOR || !process.stdout.isTTY) return "none";
|
|
27
|
+
if (/truecolor|24bit/i.test(process.env.COLORTERM ?? "")) return "truecolor";
|
|
28
|
+
return "basic";
|
|
29
|
+
}
|
|
30
|
+
var STOPS = [
|
|
31
|
+
[124, 92, 246],
|
|
32
|
+
// violet
|
|
33
|
+
[190, 75, 219],
|
|
34
|
+
// magenta
|
|
35
|
+
[232, 74, 143],
|
|
36
|
+
// pink
|
|
37
|
+
[225, 51, 74],
|
|
38
|
+
// red
|
|
39
|
+
[248, 96, 76]
|
|
40
|
+
// coral / bright red
|
|
41
|
+
];
|
|
42
|
+
function gradient(t) {
|
|
43
|
+
const n = STOPS.length - 1;
|
|
44
|
+
const scaled = Math.min(Math.max(t, 0), 1) * n;
|
|
45
|
+
const i = Math.min(Math.floor(scaled), n - 1);
|
|
46
|
+
const local = scaled - i;
|
|
47
|
+
const [a, b] = [STOPS[i], STOPS[i + 1]];
|
|
48
|
+
return [0, 1, 2].map((k) => Math.round(a[k] + (b[k] - a[k]) * local));
|
|
49
|
+
}
|
|
50
|
+
function paint(ch, t, mode) {
|
|
51
|
+
if (ch === " ") return " ";
|
|
52
|
+
if (mode === "none") return ch;
|
|
53
|
+
if (mode === "basic") return t < 0.5 ? pc.magentaBright(ch) : pc.redBright(ch);
|
|
54
|
+
const [r, g, b] = gradient(t);
|
|
55
|
+
return `\x1B[38;2;${r};${g};${b}m${ch}\x1B[39m`;
|
|
56
|
+
}
|
|
57
|
+
function compact(mode) {
|
|
58
|
+
return " " + (mode === "none" ? "partweave" : word2tone("partweave", mode));
|
|
59
|
+
}
|
|
60
|
+
function word2tone(word, mode) {
|
|
61
|
+
if (mode === "none") return word;
|
|
62
|
+
return [...word].map((ch, i) => paint(ch, word.length > 1 ? i / (word.length - 1) : 0, mode)).join("");
|
|
63
|
+
}
|
|
64
|
+
function renderBanner() {
|
|
65
|
+
const mode = colorMode();
|
|
66
|
+
if (!process.stdout.isTTY) return compact(mode);
|
|
67
|
+
const cols = process.stdout.columns ?? 80;
|
|
68
|
+
const rows = [];
|
|
69
|
+
for (let r = 0; r < HEIGHT; r++) {
|
|
70
|
+
rows.push([...WORD].map((ch) => GLYPHS[ch]?.[r] ?? "").join(" "));
|
|
71
|
+
}
|
|
72
|
+
const width = rows[0].length;
|
|
73
|
+
if (width + 2 > cols) return compact(mode);
|
|
74
|
+
const painted = rows.map(
|
|
75
|
+
(row, r) => " " + [...row].map(
|
|
76
|
+
(ch, c) => ch === " " ? " " : paint(weaveCell(r, c), width > 1 ? c / (width - 1) : 0, mode)
|
|
77
|
+
).join("")
|
|
78
|
+
);
|
|
79
|
+
return painted.join("\n");
|
|
80
|
+
}
|
|
9
81
|
|
|
10
82
|
// src/compose.ts
|
|
11
83
|
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
@@ -26,14 +98,19 @@ function injectAtAnchor(content, anchorId, lines) {
|
|
|
26
98
|
throw new Error(`Anchor <partweave:${anchorId}> not found in target file`);
|
|
27
99
|
}
|
|
28
100
|
const indent = match[1] ?? "";
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
);
|
|
32
|
-
|
|
101
|
+
const before = content.slice(0, match.index).split("\n");
|
|
102
|
+
if (before[before.length - 1] === "") before.pop();
|
|
103
|
+
const block = /* @__PURE__ */ new Set();
|
|
104
|
+
for (let i = before.length - 1; i >= 0; i--) {
|
|
105
|
+
const trimmed = before[i].trim();
|
|
106
|
+
if (trimmed === "") break;
|
|
107
|
+
block.add(trimmed);
|
|
108
|
+
}
|
|
109
|
+
const toInsert = lines.filter((l) => !block.has(l.trim()));
|
|
33
110
|
if (toInsert.length === 0) return { content, inserted: 0 };
|
|
34
|
-
const
|
|
111
|
+
const insertion = toInsert.map((l) => l ? indent + l : "").join("\n") + "\n";
|
|
35
112
|
const anchorStart = match.index;
|
|
36
|
-
const newContent = content.slice(0, anchorStart) +
|
|
113
|
+
const newContent = content.slice(0, anchorStart) + insertion + content.slice(anchorStart);
|
|
37
114
|
return { content: newContent, inserted: toInsert.length };
|
|
38
115
|
}
|
|
39
116
|
function parseDep(dep) {
|
|
@@ -50,34 +127,35 @@ function normalizeWorkspaceDeps(deps, workspaceRange) {
|
|
|
50
127
|
return dep;
|
|
51
128
|
});
|
|
52
129
|
}
|
|
130
|
+
function higherVersion(a, b) {
|
|
131
|
+
const core = (v) => {
|
|
132
|
+
const m = v.match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
133
|
+
if (!m || m[1] === void 0) return null;
|
|
134
|
+
return [m[1], m[2], m[3]].map((n) => n === void 0 ? 0 : Number(n));
|
|
135
|
+
};
|
|
136
|
+
const ca = core(a);
|
|
137
|
+
const cb = core(b);
|
|
138
|
+
if (!ca || !cb) return a;
|
|
139
|
+
for (let i = 0; i < 3; i++) {
|
|
140
|
+
if (ca[i] !== cb[i]) return ca[i] > cb[i] ? a : b;
|
|
141
|
+
}
|
|
142
|
+
return a;
|
|
143
|
+
}
|
|
53
144
|
function mergePackageJsonDeps(pkgJson, deps, field = "dependencies") {
|
|
54
145
|
if (deps.length === 0) return pkgJson;
|
|
55
146
|
const pkg = JSON.parse(pkgJson);
|
|
56
147
|
const bucket = pkg[field] ?? {};
|
|
57
148
|
for (const dep of deps) {
|
|
58
149
|
const { name, version } = parseDep(dep);
|
|
59
|
-
|
|
150
|
+
bucket[name] = name in bucket ? higherVersion(bucket[name], version) : version;
|
|
60
151
|
}
|
|
61
152
|
pkg[field] = Object.fromEntries(
|
|
62
153
|
Object.entries(bucket).sort(([a], [b]) => a.localeCompare(b))
|
|
63
154
|
);
|
|
64
155
|
return JSON.stringify(pkg, null, 2) + "\n";
|
|
65
156
|
}
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
if (keys.length === 0) return envBody;
|
|
69
|
-
const present = new Set(
|
|
70
|
-
envBody.split("\n").map((l) => l.split("=")[0]?.trim()).filter(Boolean)
|
|
71
|
-
);
|
|
72
|
-
const missing = keys.filter((k) => !present.has(k));
|
|
73
|
-
if (missing.length === 0) return envBody;
|
|
74
|
-
let out = envBody.replace(/\n*$/, "\n");
|
|
75
|
-
if (heading) out += `
|
|
76
|
-
# ${heading}
|
|
77
|
-
`;
|
|
78
|
-
for (const k of missing) out += `${k}=${entries[k]}
|
|
79
|
-
`;
|
|
80
|
-
return out;
|
|
157
|
+
function pyDepName(dep) {
|
|
158
|
+
return dep.trim().replace(/^["']|["']$/g, "").split(/[<>=!~;[\s]/)[0].trim().toLowerCase();
|
|
81
159
|
}
|
|
82
160
|
|
|
83
161
|
// src/fsutil.ts
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
529
|
-
if (
|
|
530
|
-
if (
|
|
531
|
-
return
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
"
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
"#
|
|
551
|
-
"
|
|
552
|
-
|
|
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
|
|
555
|
-
"#
|
|
556
|
-
"#
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
"# In dev the app auto-detects your
|
|
570
|
-
"#
|
|
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
|
-
|
|
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(
|
|
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("
|
|
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
|
|
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
|
-
|
|
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
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
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
|
|
1414
|
+
import pc2 from "picocolors";
|
|
1235
1415
|
import { resolve as resolve2 } from "path";
|
|
1236
|
-
var MULTI_HINT =
|
|
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
|
|
1317
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1461
|
-
steps.join("\n") + "\n\n" +
|
|
1687
|
+
note2(
|
|
1688
|
+
steps.join("\n") + "\n\n" + pc3.dim("These work on macOS, Linux & Windows. On macOS/Linux, `make <task>` works too."),
|
|
1462
1689
|
"Next steps"
|
|
1463
1690
|
);
|
|
1464
|
-
if (result.notes.length)
|
|
1465
|
-
outro(
|
|
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-
|
|
1712
|
+
//# sourceMappingURL=chunk-Y5FZVVV6.js.map
|
package/dist/create-bin.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
selectedTargets,
|
|
16
16
|
validateApps,
|
|
17
17
|
writeProjectManifest
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-Y5FZVVV6.js";
|
|
19
19
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
import { Command } from "commander";
|
|
@@ -178,7 +178,7 @@ async function runDoctor(flags) {
|
|
|
178
178
|
function buildProgram() {
|
|
179
179
|
const program = new Command();
|
|
180
180
|
program.name("partweave").description("A modular full-stack scaffolder \u2014 pick parts, generate only that code.").version("0.1.0");
|
|
181
|
-
program.command("create", { isDefault: true }).argument("[name]", "project name").description("Scaffold a new project").option("-d, --dir <path>", "target directory").option("--server", "include the Django server").option("--no-server", "exclude the server").option("--web", "include the Next.js web app").option("--no-web", "exclude the web app").option("--mobile", "include the Expo mobile app").option("--no-mobile", "exclude the mobile app").option("--with <ids>", "comma-separated component ids (e.g. auth,docker,ci)").option("--js-pm <pm>", "JS package manager: pnpm | npm (default: auto-detect)").option("--py-pm <pm>", "Python package manager: uv | pip (default: auto-detect)").option("-y, --yes", "skip prompts; use flags/defaults").option("-f, --force", "write into a non-empty directory").option("--install", "install dependencies after scaffolding (default: ask, off with --yes)").action(async (name, opts) => {
|
|
181
|
+
program.command("create", { isDefault: true }).argument("[name]", "project name").description("Scaffold a new project").option("-d, --dir <path>", "target directory").option("--server", "include the Django server").option("--no-server", "exclude the server").option("--web", "include the Next.js web app").option("--no-web", "exclude the web app").option("--mobile", "include the Expo mobile app").option("--no-mobile", "exclude the mobile app").option("--with <ids>", "comma-separated component ids (e.g. auth,docker,ci)").option("--js-pm <pm>", "JS package manager: pnpm | npm (default: auto-detect)").option("--py-pm <pm>", "Python package manager: uv | pip (default: auto-detect)").option("-y, --yes", "skip prompts; use flags/defaults").option("-f, --force", "write into a non-empty directory").option("--install", "install dependencies after scaffolding (default: ask, off with --yes)").option("--git", "initialize a git repo + initial commit (default: ask, off with --yes)").option("--no-git", "skip git initialization").action(async (name, opts) => {
|
|
182
182
|
const flags = { ...opts, name: name ?? opts.name };
|
|
183
183
|
await runCreate(flags);
|
|
184
184
|
});
|
|
@@ -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:
|
|
19
|
+
"PostgreSQL: DATABASE_URL lives in apps/server/.env (created for you, with a matching .env.example).",
|
|
20
20
|
"Need a local database? Include the 'docker' component and run `make db-up`."
|
|
21
21
|
]
|
|
22
22
|
}
|
|
@@ -14,6 +14,6 @@
|
|
|
14
14
|
},
|
|
15
15
|
"notes": [
|
|
16
16
|
"Local DB: `make db-up` (docker compose). Server image: `docker build apps/server`.",
|
|
17
|
-
"Postgres creds/port come from
|
|
17
|
+
"Postgres creds/port come from the root `.env` (POSTGRES_USER/PASSWORD/DB/PORT); keep them in sync with DATABASE_URL in apps/server/.env."
|
|
18
18
|
]
|
|
19
19
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "partweave",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "A modular full-stack scaffolder — pick the parts you need (Django server, Next.js web, Expo mobile) and generate only that code, wired together.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"license": "
|
|
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",
|