@bull-js/cli 0.0.0-stage → 0.3.0

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.
@@ -0,0 +1,356 @@
1
+ // src/config.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { loadConfigFromFile } from "vite";
5
+ function defineConfig(config) {
6
+ return config;
7
+ }
8
+ var CONFIG_FILES = ["bull.config.ts", "bull.config.mts", "bull.config.js", "bull.config.mjs"];
9
+ async function loadBullConfig(root, command, options = {}) {
10
+ let file;
11
+ if (options.configFile) {
12
+ file = path.resolve(options.configFile);
13
+ if (!fs.existsSync(file)) throw new Error(`The config file ${options.configFile} does not exist.`);
14
+ } else {
15
+ file = CONFIG_FILES.map((name) => path.join(root, name)).find((p) => fs.existsSync(p));
16
+ }
17
+ if (!file) return { config: {} };
18
+ try {
19
+ const loaded = await loadConfigFromFile(
20
+ { command, mode: options.mode ?? (command === "build" ? "production" : "development") },
21
+ file,
22
+ root,
23
+ "silent",
24
+ void 0,
25
+ // "runner" executes the config with Vite's module runner, without bundling it.
26
+ "runner"
27
+ );
28
+ return { config: loaded?.config ?? {}, file };
29
+ } catch (error) {
30
+ const message = error.message.split("\n")[0];
31
+ const hint = /Cannot find (package|module) '@bull-js\//.test(message ?? "") ? "Did you run `npm install`? The config imports from @bull-js/cli." : "Make sure it default-exports `defineConfig({ ... })`.";
32
+ throw new Error(`Could not load ${path.basename(file)}: ${message}
33
+ ${hint}`);
34
+ }
35
+ }
36
+
37
+ // src/vite/plugin.ts
38
+ import {
39
+ formatReport,
40
+ normalizePath,
41
+ ProjectAnalyzer,
42
+ serializeResult
43
+ } from "@bull-js/core";
44
+ import pc from "picocolors";
45
+
46
+ // src/vite/overlay-client.ts
47
+ function overlayMain(hot) {
48
+ if (!hot || typeof customElements === "undefined") return;
49
+ const css = `
50
+ :host { all: initial; }
51
+ * { box-sizing: border-box; }
52
+ .backdrop {
53
+ position: fixed; inset: 0; z-index: 2147483646; overflow-y: auto;
54
+ background: rgba(9, 9, 14, 0.72); backdrop-filter: blur(6px);
55
+ font: 14px/1.55 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
56
+ color: #e6e6ef; padding: 48px 16px;
57
+ }
58
+ .panel { max-width: 920px; margin: 0 auto; }
59
+ header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }
60
+ .logo { display: block; width: 40px; height: 40px; flex: none; }
61
+ header h1 { font-size: 18px; margin: 0; font-weight: 650; letter-spacing: -0.01em; }
62
+ header p { margin: 0; color: #9a9ab0; font-size: 13px; }
63
+ .spacer { flex: 1; }
64
+ .close {
65
+ border: 1px solid #34344a; background: #1b1b27; color: #c8c8da; border-radius: 8px;
66
+ padding: 6px 12px; cursor: pointer; font: inherit; font-size: 13px;
67
+ }
68
+ .close:hover { background: #26263a; }
69
+ .arch {
70
+ display: flex; gap: 8px; align-items: center; margin: 0 0 20px; padding: 10px 14px;
71
+ background: #14141d; border: 1px solid #262636; border-radius: 10px; font-size: 13px; color: #9a9ab0;
72
+ flex-wrap: wrap;
73
+ }
74
+ .layer { padding: 2px 10px; border-radius: 999px; background: #1f2a22; color: #7ee2a8; font-weight: 600; }
75
+ .arrow { color: #4c8f69; }
76
+ .card {
77
+ background: #15151f; border: 1px solid #2a2a3c; border-left: 4px solid var(--tone);
78
+ border-radius: 12px; padding: 18px 20px; margin-bottom: 16px;
79
+ }
80
+ .card.error { --tone: #ff5c7a; }
81
+ .card.warning { --tone: #ffc04d; }
82
+ .card-head { display: flex; gap: 12px; align-items: baseline; flex-wrap: wrap; }
83
+ .title { color: var(--tone); font-weight: 750; font-size: 13px; letter-spacing: 0.04em; }
84
+ .code { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; color: #8a8aa3; }
85
+ .loc {
86
+ margin-left: auto; font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; color: #a99bff;
87
+ background: none; border: 0; padding: 0; cursor: pointer; text-decoration: underline dotted;
88
+ }
89
+ .message { font-size: 16px; font-weight: 600; margin: 10px 0 12px; color: #fff; }
90
+ pre {
91
+ margin: 0 0 14px; padding: 12px 0; background: #0d0d14; border-radius: 8px; overflow-x: auto;
92
+ font: 12.5px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
93
+ }
94
+ .fl { display: block; padding: 0 14px; white-space: pre; color: #8d8da6; }
95
+ .fl.hl { color: #f0f0f8; background: rgba(255, 92, 122, 0.08); }
96
+ .warning .fl.hl { background: rgba(255, 192, 77, 0.08); }
97
+ .ln { display: inline-block; min-width: 3ch; margin-right: 14px; color: #55556d; text-align: right; }
98
+ mark { background: none; color: inherit; text-decoration: underline wavy var(--tone); text-underline-offset: 4px; }
99
+ .why { color: #b9b9cc; margin: 0 0 12px; }
100
+ .notes { font: 12.5px ui-monospace, SFMono-Regular, Menlo, monospace; color: #b9b9cc; margin: 0 0 12px; white-space: pre; }
101
+ .fix-title { font-weight: 700; color: #7ee2a8; font-size: 13px; margin-bottom: 4px; }
102
+ ol { margin: 0 0 10px; padding-left: 22px; color: #dadaea; }
103
+ li { margin: 2px 0; }
104
+ .sub { list-style: none; margin-left: -22px; padding-left: 22px; }
105
+ code {
106
+ font: 12.5px ui-monospace, SFMono-Regular, Menlo, monospace; background: #232333;
107
+ padding: 1px 6px; border-radius: 5px; color: #ffd4e0;
108
+ }
109
+ .explain { font-size: 12px; color: #6f6f88; }
110
+ .fixable { font-size: 12.5px; color: #8fd3ff; margin-bottom: 6px; }
111
+ .pill {
112
+ position: fixed; right: 16px; bottom: 16px; z-index: 2147483646; cursor: pointer;
113
+ display: flex; gap: 8px; align-items: center; border: 1px solid #3a2330;
114
+ background: #1d1218; color: #ffb3c2; border-radius: 999px; padding: 8px 14px;
115
+ font: 600 13px ui-sans-serif, system-ui, sans-serif; box-shadow: 0 8px 24px rgba(0,0,0,.35);
116
+ }
117
+ .pill.warning { background: #1f1a10; color: #ffd98a; border-color: #3d3220; }
118
+ .dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
119
+ `;
120
+ const bull = `<svg class="logo" viewBox="0 0 64 64" aria-hidden="true">
121
+ <path d="M19 20C10.5 20 4.5 15 4 5.5c3.5 5.8 9 8.6 17 9.2Z" fill="#ffd9bf"/>
122
+ <path d="M45 20c8.5 0 14.5-5 15-14.5c-3.5 5.8-9 8.6-17 9.2Z" fill="#ffd9bf"/>
123
+ <path d="M19.5 21 9 24.5l11.5 4Z" fill="#5a3df0"/><path d="M44.5 21 55 24.5l-11.5 4Z" fill="#e2456f"/>
124
+ <path d="M18 15h28L32 36Z" fill="#a78bfa"/>
125
+ <path d="M18 15l14 21v21h-6L20.5 34Z" fill="#6b4dff"/>
126
+ <path d="M46 15 32 36v21h6l5.5-23Z" fill="#f0507c"/>
127
+ <path d="m23.5 28.5 4.5 1.5-4 1.8Z" fill="#160f24"/><path d="m40.5 28.5-4.5 1.5 4 1.8Z" fill="#160f24"/>
128
+ <rect x="23" y="45" width="18" height="12" rx="6" fill="#33224a"/>
129
+ <ellipse cx="28" cy="50.5" rx="1.6" ry="2.2" fill="#ff7aa2"/><ellipse cx="36" cy="50.5" rx="1.6" ry="2.2" fill="#ff7aa2"/>
130
+ <path d="M28.5 56.5a3.5 3.5 0 0 0 7 0" fill="none" stroke="#fff4e6" stroke-width="1.8" stroke-linecap="round"/>
131
+ </svg>`;
132
+ const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
133
+ const inline = (s) => esc(s).replace(/`([^`]+)`/g, "<code>$1</code>");
134
+ function frameHtml(frame) {
135
+ const rows = frame.map((l) => {
136
+ let text = esc(l.text);
137
+ if (l.highlight) {
138
+ const [from, to] = l.highlight;
139
+ const raw = l.text;
140
+ text = esc(raw.slice(0, from - 1)) + "<mark>" + esc(raw.slice(from - 1, to - 1) || " ") + "</mark>" + esc(raw.slice(to - 1));
141
+ }
142
+ return `<span class="fl${l.highlight ? " hl" : ""}"><span class="ln">${l.line}</span>${text}</span>`;
143
+ });
144
+ return `<pre>${rows.join("")}</pre>`;
145
+ }
146
+ function cardHtml(d, i) {
147
+ const loc = d.file ? `${d.file}${d.line ? `:${d.line}:${d.column}` : ""}` : "";
148
+ const fix = d.fix.map((f) => f.startsWith(" ") ? `<li class="sub">${inline(f.trim())}</li>` : `<li>${inline(f)}</li>`).join("");
149
+ return `
150
+ <section class="card ${d.severity}">
151
+ <div class="card-head">
152
+ <span class="title">${esc(d.title)}</span>
153
+ <span class="code">${esc(d.code)}</span>
154
+ ${loc ? `<button class="loc" data-open="${i}" title="Open in editor">${esc(loc)}</button>` : ""}
155
+ </div>
156
+ <div class="message">${inline(d.message)}</div>
157
+ ${d.frame ? frameHtml(d.frame) : ""}
158
+ ${d.notes?.length ? `<div class="notes">${esc(d.notes.join("\n"))}</div>` : ""}
159
+ <p class="why">${inline(d.why)}</p>
160
+ <div class="fix-title">How to fix</div>
161
+ <ol>${fix}</ol>
162
+ ${d.fixable ? `<div class="fixable">\u2726 Auto-fixable: run <code>bull fix</code></div>` : ""}
163
+ <div class="explain">More info in the terminal: <code>${esc(d.explain)}</code></div>
164
+ </section>`;
165
+ }
166
+ class BullOverlay extends HTMLElement {
167
+ payload = null;
168
+ collapsed = false;
169
+ root;
170
+ constructor() {
171
+ super();
172
+ this.root = this.attachShadow({ mode: "open" });
173
+ this.root.addEventListener("click", (event) => this.onClick(event));
174
+ window.addEventListener("keydown", (event) => {
175
+ if (event.key === "Escape" && this.payload?.mode !== "blocking") this.collapse(true);
176
+ });
177
+ }
178
+ update(payload) {
179
+ const changed = JSON.stringify(payload.diagnostics) !== JSON.stringify(this.payload?.diagnostics);
180
+ this.payload = payload;
181
+ if (changed && payload.errorCount > 0) this.collapsed = false;
182
+ if (payload.errorCount === 0 && payload.diagnostics.length > 0 && changed) this.collapsed = true;
183
+ this.render();
184
+ }
185
+ collapse(value) {
186
+ this.collapsed = value;
187
+ this.render();
188
+ }
189
+ onClick(event) {
190
+ const target = event.target;
191
+ if (target.closest(".close")) {
192
+ this.collapse(true);
193
+ return;
194
+ }
195
+ if (target.closest(".pill")) {
196
+ this.collapse(false);
197
+ return;
198
+ }
199
+ const open = target.closest("[data-open]");
200
+ if (open && this.payload) {
201
+ const d = this.payload.diagnostics[Number(open.dataset.open)];
202
+ if (d?.absoluteFile) {
203
+ void fetch(
204
+ `/__open-in-editor?file=${encodeURIComponent(`${d.absoluteFile}:${d.line ?? 1}:${d.column ?? 1}`)}`
205
+ );
206
+ }
207
+ }
208
+ }
209
+ render() {
210
+ const p = this.payload;
211
+ if (!p || p.diagnostics.length === 0) {
212
+ this.root.innerHTML = "";
213
+ return;
214
+ }
215
+ const tone = p.errorCount ? "error" : "warning";
216
+ const summary = [
217
+ p.errorCount ? `${p.errorCount} ${p.errorCount === 1 ? "error" : "errors"}` : "",
218
+ p.warningCount ? `${p.warningCount} ${p.warningCount === 1 ? "warning" : "warnings"}` : ""
219
+ ].filter(Boolean).join(" \xB7 ");
220
+ if (this.collapsed && p.mode !== "blocking") {
221
+ this.root.innerHTML = `<style>${css}</style><button class="pill ${tone}"><span class="dot"></span>MVVM \xB7 ${summary}</button>`;
222
+ return;
223
+ }
224
+ this.root.innerHTML = `
225
+ <style>${css}</style>
226
+ <div class="backdrop" role="dialog" aria-label="MVVM architecture errors">
227
+ <div class="panel">
228
+ <header>
229
+ ${bull}
230
+ <div>
231
+ <h1>The MVVM architecture is broken</h1>
232
+ <p>${summary}${p.mode === "blocking" ? " \xB7 the app unlocks once you fix it" : " \xB7 Esc to close"}</p>
233
+ </div>
234
+ <div class="spacer"></div>
235
+ ${p.mode === "blocking" ? "" : `<button class="close">Close</button>`}
236
+ </header>
237
+ <div class="arch">
238
+ Allowed direction:
239
+ <span class="layer">View</span><span class="arrow">\u2500\u2500\u25B6</span>
240
+ <span class="layer">ViewModel</span><span class="arrow">\u2500\u2500\u25B6</span>
241
+ <span class="layer">Model</span>
242
+ </div>
243
+ ${p.diagnostics.map(cardHtml).join("")}
244
+ </div>
245
+ </div>`;
246
+ }
247
+ }
248
+ const TAG = "bull-mvvm-overlay";
249
+ if (!customElements.get(TAG)) customElements.define(TAG, BullOverlay);
250
+ let element = document.querySelector(TAG);
251
+ const ensure = () => {
252
+ if (!element) {
253
+ element = document.createElement(TAG);
254
+ document.documentElement.appendChild(element);
255
+ }
256
+ return element;
257
+ };
258
+ hot.on("bull:diagnostics", (payload) => ensure().update(payload));
259
+ hot.send("bull:request");
260
+ }
261
+ var OVERLAY_PUBLIC_PATH = "/@bull-js/overlay.js";
262
+ function overlayModuleCode() {
263
+ return `(${overlayMain.toString()})(import.meta.hot);
264
+ `;
265
+ }
266
+
267
+ // src/vite/plugin.ts
268
+ var VIRTUAL_OVERLAY = "\0bull-overlay";
269
+ function bullPlugin(options = {}) {
270
+ let analyzer = options.analyzer;
271
+ let isBuild = false;
272
+ let last;
273
+ let lastKey = "";
274
+ const overlayMode = options.overlay ?? "dismissible";
275
+ const payload = () => {
276
+ const s = serializeResult(last);
277
+ return {
278
+ errorCount: s.errorCount,
279
+ warningCount: s.warningCount,
280
+ diagnostics: s.diagnostics,
281
+ mode: overlayMode || "dismissible"
282
+ };
283
+ };
284
+ function runDev(server, initial = false) {
285
+ const previous = last;
286
+ last = analyzer.analyze();
287
+ const key = JSON.stringify(last.diagnostics);
288
+ if (key === lastKey && !initial) return;
289
+ lastKey = key;
290
+ if (last.diagnostics.length > 0) {
291
+ console.log(`
292
+ ${formatReport(last, { width: Math.min(process.stdout.columns || 88, 100) })}`);
293
+ } else if (previous && previous.diagnostics.length > 0) {
294
+ console.log(`
295
+ ${pc.green("\u2713")} ${pc.bold("MVVM architecture ok")} ${pc.dim("\xB7 every violation was fixed")}
296
+ `);
297
+ }
298
+ if (overlayMode) server.ws.send("bull:diagnostics", payload());
299
+ }
300
+ return {
301
+ name: "bull:mvvm",
302
+ enforce: "pre",
303
+ configResolved(config) {
304
+ isBuild = config.command === "build";
305
+ analyzer ??= new ProjectAnalyzer(options.root ?? config.root, options.config);
306
+ },
307
+ buildStart() {
308
+ if (!isBuild || options.checkOnBuild === false) return;
309
+ const result = analyzer.analyze();
310
+ if (result.diagnostics.length) console.log(`
311
+ ${formatReport(result)}`);
312
+ if (result.errorCount > 0) {
313
+ this.error(`The MVVM architecture has ${result.errorCount} error(s). Fix them to be able to build.`);
314
+ }
315
+ },
316
+ configureServer(server) {
317
+ const { srcDir, compositionRoot } = analyzer.config;
318
+ let timer;
319
+ const schedule = () => {
320
+ clearTimeout(timer);
321
+ timer = setTimeout(() => runDev(server), 40);
322
+ };
323
+ server.watcher.on("all", (event, file) => {
324
+ const abs = normalizePath(file);
325
+ if (!abs.startsWith(`${srcDir}/`) && abs !== srcDir && abs !== compositionRoot) return;
326
+ if (event === "change" || event === "unlink") analyzer.invalidate(abs);
327
+ schedule();
328
+ });
329
+ if (overlayMode) {
330
+ server.ws.on("bull:request", (_data, client) => {
331
+ if (last) client.send("bull:diagnostics", payload());
332
+ });
333
+ }
334
+ runDev(server, true);
335
+ },
336
+ resolveId(id) {
337
+ if (id === OVERLAY_PUBLIC_PATH) return VIRTUAL_OVERLAY;
338
+ },
339
+ load(id) {
340
+ if (id === VIRTUAL_OVERLAY) return overlayModuleCode();
341
+ },
342
+ transformIndexHtml: {
343
+ order: "pre",
344
+ handler(_html, ctx) {
345
+ if (!ctx.server || !overlayMode) return;
346
+ return [{ tag: "script", attrs: { type: "module", src: OVERLAY_PUBLIC_PATH }, injectTo: "head" }];
347
+ }
348
+ }
349
+ };
350
+ }
351
+
352
+ export {
353
+ defineConfig,
354
+ loadBullConfig,
355
+ bullPlugin
356
+ };
@@ -0,0 +1,259 @@
1
+ import {
2
+ bullPlugin,
3
+ loadBullConfig
4
+ } from "./chunk-FHBUQ5I5.js";
5
+
6
+ // src/ui/brand.ts
7
+ import fs from "fs";
8
+ import path from "path";
9
+ import { fileURLToPath } from "url";
10
+ import pc from "picocolors";
11
+ function readVersion() {
12
+ let dir = path.dirname(fileURLToPath(import.meta.url));
13
+ for (let i = 0; i < 4; i++) {
14
+ const file = path.join(dir, "package.json");
15
+ if (fs.existsSync(file)) {
16
+ const pkg = JSON.parse(fs.readFileSync(file, "utf8"));
17
+ if (pkg.name === "@bull-js/cli" && pkg.version) return pkg.version;
18
+ }
19
+ dir = path.dirname(dir);
20
+ }
21
+ return "0.0.0";
22
+ }
23
+ var VERSION = readVersion();
24
+ var TAGLINE = "Stubborn about architecture.";
25
+ var BULL = "\u2649\uFE0E";
26
+ var ART = [
27
+ "\u2584\u2584 \u2584\u2584",
28
+ " \u2580\u2588\u2584\u2584 \u2584\u2584\u2588\u2580",
29
+ " \u2580\u2588\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2588\u2580",
30
+ " \u2588 o o \u2588",
31
+ " \u2588 \u2584 \u2588",
32
+ " \u2580\u2584\u2584\u2584\u2580"
33
+ ];
34
+ function bullArt(subtitle) {
35
+ const side = [
36
+ "",
37
+ "",
38
+ `${pc.bold("bull")} ${pc.dim(`v${VERSION}`)}`,
39
+ pc.dim(TAGLINE),
40
+ subtitle ? pc.cyan(subtitle) : "",
41
+ ""
42
+ ];
43
+ return ART.map((line, i) => {
44
+ const text2 = side[i] ? `${" ".repeat(21 - line.length)}${side[i]}` : "";
45
+ return ` ${pc.magenta(line)}${text2}`;
46
+ }).join("\n");
47
+ }
48
+ function banner(command) {
49
+ console.log(`
50
+ ${pc.bold(pc.magenta(`${BULL} bull`))} ${pc.dim(`v${VERSION}`)} ${pc.dim("\xB7")} ${command}
51
+ `);
52
+ }
53
+
54
+ // src/commands/create.ts
55
+ import { spawnSync } from "child_process";
56
+ import fs3 from "fs";
57
+ import path3 from "path";
58
+ import { fileURLToPath as fileURLToPath2 } from "url";
59
+ import * as p from "@clack/prompts";
60
+ import pc3 from "picocolors";
61
+
62
+ // src/commands/shared.ts
63
+ import fs2 from "fs";
64
+ import path2 from "path";
65
+ import { formatReport, ProjectAnalyzer } from "@bull-js/core";
66
+ import react from "@vitejs/plugin-react";
67
+ import pc2 from "picocolors";
68
+ import { mergeConfig } from "vite";
69
+ function fail(message, hint) {
70
+ console.error(` ${pc2.red("\u2717")} ${message}`);
71
+ if (hint) console.error(` ${pc2.dim(hint)}`);
72
+ process.exitCode = 1;
73
+ }
74
+ async function loadProject(rootArg, command, options = {}) {
75
+ const root = path2.resolve(rootArg ?? ".");
76
+ const { config, file } = await loadBullConfig(root, command, options);
77
+ return { root, config, configFile: file, mode: options.mode, analyzer: new ProjectAnalyzer(root, config) };
78
+ }
79
+ function runAnalysis(project) {
80
+ const result = project.analyzer.analyze();
81
+ console.log(formatReport(result, { width: Math.min(process.stdout.columns || 88, 100) }));
82
+ return result;
83
+ }
84
+ function ensureIndexHtml(root) {
85
+ if (fs2.existsSync(path2.join(root, "index.html"))) return true;
86
+ fail(
87
+ "index.html was not found at the project root.",
88
+ 'Create one with <div id="root"></div> and <script type="module" src="/src/main.tsx"></script>.'
89
+ );
90
+ return false;
91
+ }
92
+ function viteConfig(project, extra = {}) {
93
+ const base = {
94
+ root: project.root,
95
+ mode: project.mode,
96
+ configFile: false,
97
+ clearScreen: false,
98
+ // @bull-js/runtime/react imports react-dom/client. If Vite discovers it late, it
99
+ // optimizes it separately and the app ends up with two copies of React ("Invalid hook call").
100
+ resolve: { dedupe: ["react", "react-dom"] },
101
+ optimizeDeps: {
102
+ include: ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "react/jsx-dev-runtime"]
103
+ },
104
+ plugins: [
105
+ react(),
106
+ bullPlugin({
107
+ analyzer: project.analyzer,
108
+ overlay: project.config.dev?.overlay,
109
+ checkOnBuild: false
110
+ })
111
+ ]
112
+ };
113
+ return mergeConfig(mergeConfig(base, extra), project.config.vite ?? {});
114
+ }
115
+
116
+ // src/commands/create.ts
117
+ var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
118
+ function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
119
+ const name = userAgent?.split(" ")[0]?.split("/")[0];
120
+ return PACKAGE_MANAGERS.includes(name) ? name : "npm";
121
+ }
122
+ function runScript(pm, script) {
123
+ return pm === "npm" ? `npm run ${script}` : `${pm} ${script}`;
124
+ }
125
+ function templatesDir() {
126
+ let dir = path3.dirname(fileURLToPath2(import.meta.url));
127
+ for (let i = 0; i < 4; i++) {
128
+ const candidate = path3.join(dir, "templates");
129
+ if (fs3.existsSync(candidate)) return candidate;
130
+ dir = path3.dirname(dir);
131
+ }
132
+ throw new Error("Bull templates were not found.");
133
+ }
134
+ function copyTemplate(from, to, vars) {
135
+ const written = [];
136
+ fs3.mkdirSync(to, { recursive: true });
137
+ for (const entry of fs3.readdirSync(from, { withFileTypes: true })) {
138
+ const name = entry.name === "_gitignore" ? ".gitignore" : entry.name;
139
+ const src = path3.join(from, entry.name);
140
+ const dest = path3.join(to, name);
141
+ if (entry.isDirectory()) {
142
+ written.push(...copyTemplate(src, dest, vars));
143
+ } else {
144
+ const content = fs3.readFileSync(src, "utf8").replace(/\{\{(\w+)\}\}/g, (m, key) => vars[key] ?? m);
145
+ fs3.writeFileSync(dest, content);
146
+ written.push(dest);
147
+ }
148
+ }
149
+ return written;
150
+ }
151
+ function scaffoldProject({ name, template = "default" }) {
152
+ const target = path3.resolve(name);
153
+ if (fs3.existsSync(target) && fs3.readdirSync(target).length > 0) {
154
+ throw new Error(`The folder ${pc3.bold(name)} already exists and is not empty.`);
155
+ }
156
+ const from = path3.join(templatesDir(), template);
157
+ if (!fs3.existsSync(from)) throw new Error(`The template "${template}" does not exist.`);
158
+ const pkgName = path3.basename(target).toLowerCase().replace(/[^a-z0-9-_.]/g, "-");
159
+ return { target, files: copyTemplate(from, target, { name: pkgName, bullVersion: VERSION }) };
160
+ }
161
+ function run(command, args, cwd, stdio = "ignore") {
162
+ const result = spawnSync(command, args, { cwd, stdio, shell: process.platform === "win32" });
163
+ return result.status === 0;
164
+ }
165
+ function initGit(target) {
166
+ if (!run("git", ["--version"], target)) return false;
167
+ if (run("git", ["rev-parse", "--is-inside-work-tree"], target)) return false;
168
+ if (!run("git", ["init", "-q"], target)) return false;
169
+ run("git", ["add", "-A"], target);
170
+ run("git", ["commit", "-q", "-m", "Initial commit from bull create"], target);
171
+ return true;
172
+ }
173
+ async function createCommand(nameArg, options) {
174
+ console.log(`
175
+ ${bullArt("create")}
176
+ `);
177
+ let name = nameArg;
178
+ let install = options.install ?? true;
179
+ let git = options.git ?? true;
180
+ if (!name) {
181
+ if (!process.stdin.isTTY || p.isCI()) {
182
+ fail("Missing the project name.", "Usage: bull create <name> [--no-install] [--no-git] [--pm npm|pnpm|yarn|bun]");
183
+ return;
184
+ }
185
+ const answers = await p.group(
186
+ {
187
+ name: () => p.text({
188
+ message: "Project name",
189
+ placeholder: "my-app",
190
+ defaultValue: "my-app",
191
+ validate: (value) => value && /[<>:"|?*]/.test(value) ? "Use letters, numbers and dashes." : void 0
192
+ }),
193
+ install: () => p.confirm({ message: "Install dependencies?", initialValue: install }),
194
+ git: () => p.confirm({ message: "Initialize a git repository?", initialValue: git })
195
+ },
196
+ { onCancel: () => p.cancel("Cancelled.") }
197
+ );
198
+ if (p.isCancel(answers.name) || answers.name === void 0) return;
199
+ name = answers.name;
200
+ install = answers.install === true;
201
+ git = answers.git === true;
202
+ }
203
+ if (options.pm && !PACKAGE_MANAGERS.includes(options.pm)) {
204
+ fail(`Unknown package manager "${options.pm}".`, `Use one of: ${PACKAGE_MANAGERS.join(", ")}.`);
205
+ return;
206
+ }
207
+ const pm = options.pm ?? detectPackageManager();
208
+ let scaffolded;
209
+ try {
210
+ scaffolded = scaffoldProject({ name, template: options.template });
211
+ } catch (error) {
212
+ fail(error.message);
213
+ return;
214
+ }
215
+ const { target, files } = scaffolded;
216
+ const rel = (f) => path3.relative(target, f).replace(/\\/g, "/");
217
+ console.log(` ${pc3.green("\u2713")} Project created in ${pc3.bold(path3.relative(process.cwd(), target) || ".")}
218
+ `);
219
+ const tree = files.map(rel).filter((f) => f.startsWith("src/")).sort();
220
+ for (const f of tree) {
221
+ const layer = f.split("/")[1];
222
+ const color = layer === "model" ? pc3.blue : layer === "viewmodel" ? pc3.magenta : layer === "view" ? pc3.green : pc3.dim;
223
+ console.log(` ${color(f)}`);
224
+ }
225
+ let installed = false;
226
+ if (install) {
227
+ console.log(`
228
+ ${pc3.dim(`Installing dependencies with ${pm}\u2026`)}
229
+ `);
230
+ installed = run(pm, ["install"], target, "inherit");
231
+ if (!installed) console.log(`
232
+ ${pc3.yellow("!")} ${pm} install failed. Run it yourself inside the folder.`);
233
+ }
234
+ if (git && initGit(target)) console.log(`
235
+ ${pc3.green("\u2713")} Initialized a git repository`);
236
+ const steps = [`cd ${name}`, ...installed ? [] : [`${pm} install`], runScript(pm, "dev")];
237
+ console.log(`
238
+ Next steps:
239
+ ${steps.map((s) => ` ${pc3.cyan(s)}`).join("\n")}
240
+
241
+ House rules: ${pc3.dim("View \u2500\u2500\u25B6 ViewModel \u2500\u2500\u25B6 Model")}. Break them and it will not build.
242
+ See every rule with ${pc3.cyan("npx bull explain")}.
243
+ `);
244
+ }
245
+
246
+ export {
247
+ VERSION,
248
+ bullArt,
249
+ banner,
250
+ fail,
251
+ loadProject,
252
+ runAnalysis,
253
+ ensureIndexHtml,
254
+ viteConfig,
255
+ detectPackageManager,
256
+ runScript,
257
+ scaffoldProject,
258
+ createCommand
259
+ };
@@ -0,0 +1,24 @@
1
+ type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
2
+ /** The package manager that launched us (`npm create`, `pnpm create`…), read from `npm_config_user_agent`. */
3
+ declare function detectPackageManager(userAgent?: string | undefined): PackageManager;
4
+ /** `npm run dev` for npm, `pnpm dev` / `yarn dev` / `bun dev` for the rest. */
5
+ declare function runScript(pm: PackageManager, script: string): string;
6
+ interface ScaffoldOptions {
7
+ /** Folder to create, relative to the current one. */
8
+ name: string;
9
+ template?: string;
10
+ }
11
+ /** Copies a template into a new folder. Throws if the folder is not empty or the template does not exist. */
12
+ declare function scaffoldProject({ name, template }: ScaffoldOptions): {
13
+ target: string;
14
+ files: string[];
15
+ };
16
+ interface CreateOptions {
17
+ template?: string;
18
+ install?: boolean;
19
+ git?: boolean;
20
+ pm?: string;
21
+ }
22
+ declare function createCommand(nameArg: string | undefined, options: CreateOptions): Promise<void>;
23
+
24
+ export { type CreateOptions, type PackageManager, type ScaffoldOptions, createCommand, detectPackageManager, runScript, scaffoldProject };
package/dist/create.js ADDED
@@ -0,0 +1,13 @@
1
+ import {
2
+ createCommand,
3
+ detectPackageManager,
4
+ runScript,
5
+ scaffoldProject
6
+ } from "./chunk-KF3FEGRV.js";
7
+ import "./chunk-FHBUQ5I5.js";
8
+ export {
9
+ createCommand,
10
+ detectPackageManager,
11
+ runScript,
12
+ scaffoldProject
13
+ };