@ubean/cli 0.1.2 → 0.1.4

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/dist/cli.js ADDED
@@ -0,0 +1,2272 @@
1
+ import { S as createFsOps, a as scaffold, i as recoverScaffold, n as listScaffoldableFiles, r as pageCommand, t as deleteScaffold, v as renderTemplate } from "./page-Cmp7s9l2.js";
2
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
3
+ import { extname, normalize } from "node:path";
4
+ import { consola } from "consola";
5
+ import { defineCommand, runMain } from "citty";
6
+ import { readFile, rm, stat } from "node:fs/promises";
7
+ import { pathToFileURL } from "node:url";
8
+ import { buildProduction } from "@ubean/build/production";
9
+ import { generateOpenApiTypesFromServer, generateTypes } from "@ubean/codegen";
10
+ import { loadUbeanConfig, resolvePrerenderConfig } from "@ubean/config";
11
+ import { prerender } from "@ubean/prerender";
12
+ import { NODE_REQUIREMENTS, createCapabilitySet, diagnoseCapabilities, registerBuiltinPresets, resolvePresetByName } from "@ubean/preset";
13
+ import { scanProject } from "@ubean/routing";
14
+ import { basename, join as join$1, resolve as resolve$1 } from "pathe";
15
+ import { createUbeanApp } from "@ubean/app";
16
+ import { createDevRunner, createDevWatcher, logDiagnostics } from "@ubean/dev-server";
17
+ import { bold, cyan, dim, green } from "kolorist";
18
+ import { execSync, spawn } from "node:child_process";
19
+ import { BUILTIN_MODULES, isBuiltinDisabled } from "@ubean/modules";
20
+ import { createServer } from "node:http";
21
+ import { findAvailablePort, waitForPort } from "@ubean/utils";
22
+ //#region src/config.ts
23
+ const logger$8 = consola.withTag("ubean-cli");
24
+ const CONFIG_TEMPLATE = `import { defineConfig } from 'ubean';
25
+
26
+ export default defineConfig({
27
+ srcDir: 'src',
28
+ preset: '{{preset}}'
29
+ });
30
+ `;
31
+ const CONFIG_EXAMPLE_FULL = `import { defineConfig } from 'ubean';
32
+
33
+ export default defineConfig({
34
+ // Source directory
35
+ srcDir: 'src',
36
+
37
+ // Preset: 'standard' | 'node' | 'cloudflare'
38
+ preset: 'standard',
39
+
40
+ // Dev server options
41
+ dev: {
42
+ port: 9527,
43
+ host: 'localhost',
44
+ open: false
45
+ },
46
+
47
+ // Build options
48
+ build: {
49
+ outDir: 'dist'
50
+ },
51
+
52
+ // View transitions
53
+ viewTransitions: {
54
+ enabled: true
55
+ },
56
+
57
+ // DevTools
58
+ devtools: {
59
+ enabled: true
60
+ }
61
+ });
62
+ `;
63
+ async function findConfigFile(fs) {
64
+ for (const name of [
65
+ "ubean.config.ts",
66
+ "ubean.config.js",
67
+ "ubean.config.mjs",
68
+ "ubean.config.mts"
69
+ ]) if (await fs.exists(name)) return name;
70
+ return null;
71
+ }
72
+ const configCommand = {
73
+ meta: {
74
+ name: "config",
75
+ description: "Manage ubean configuration"
76
+ },
77
+ subCommands: {
78
+ init: {
79
+ meta: {
80
+ name: "init",
81
+ description: "Create a default ubean.config.ts file"
82
+ },
83
+ args: {
84
+ preset: {
85
+ type: "string",
86
+ description: "Preset to use (standard, node, cloudflare)",
87
+ default: "standard"
88
+ },
89
+ force: {
90
+ type: "boolean",
91
+ description: "Overwrite existing config file",
92
+ default: false,
93
+ alias: "f"
94
+ },
95
+ dry: {
96
+ type: "boolean",
97
+ description: "Show what would be created",
98
+ default: false
99
+ }
100
+ },
101
+ async run({ args }) {
102
+ const fs = createFsOps(process.cwd());
103
+ const preset = args.preset;
104
+ const existing = await findConfigFile(fs);
105
+ if (existing && !args.force) {
106
+ logger$8.warn(`Config file already exists: ${existing} (use --force to overwrite)`);
107
+ return;
108
+ }
109
+ const content = renderTemplate(CONFIG_TEMPLATE, { variables: { preset } });
110
+ if (args.dry) {
111
+ logger$8.success("[dry-run] Would create ubean.config.ts");
112
+ logger$8.log(`\n${content}`);
113
+ return;
114
+ }
115
+ await fs.writeFile("ubean.config.ts", content);
116
+ logger$8.success("Created ubean.config.ts");
117
+ }
118
+ },
119
+ show: {
120
+ meta: {
121
+ name: "show",
122
+ description: "Show current configuration file location and content"
123
+ },
124
+ async run() {
125
+ const fs = createFsOps(process.cwd());
126
+ const configFile = await findConfigFile(fs);
127
+ if (!configFile) {
128
+ logger$8.warn("No ubean config file found. Run `ubean config init` to create one.");
129
+ return;
130
+ }
131
+ logger$8.info(`Config file: ${configFile}`);
132
+ logger$8.log("");
133
+ const content = await fs.readFile(configFile);
134
+ logger$8.log(content);
135
+ }
136
+ },
137
+ example: {
138
+ meta: {
139
+ name: "example",
140
+ description: "Show a full example configuration with all options"
141
+ },
142
+ async run() {
143
+ logger$8.log(CONFIG_EXAMPLE_FULL);
144
+ }
145
+ },
146
+ path: {
147
+ meta: {
148
+ name: "path",
149
+ description: "Print resolved config file path"
150
+ },
151
+ async run() {
152
+ const fs = createFsOps(process.cwd());
153
+ const configFile = await findConfigFile(fs);
154
+ if (!configFile) {
155
+ logger$8.error("No config file found");
156
+ process.exit(1);
157
+ }
158
+ logger$8.log(fs.resolve(configFile));
159
+ }
160
+ }
161
+ }
162
+ };
163
+ //#endregion
164
+ //#region src/build.ts
165
+ const logger$7 = consola.withTag("ubean-cli");
166
+ /**
167
+ * Creates a fetcher that invokes the built SSR entry to render real HTML.
168
+ * Inspired by void's Node prerender runner: import the built `entry.mjs`,
169
+ * call its `createFetchHandler()` to obtain a `fetch(req)` function, then
170
+ * drive it with synthetic `http://localhost<path>` requests.
171
+ *
172
+ * If the SSR entry cannot be loaded (e.g. missing dependencies), returns
173
+ * `undefined` so the caller falls back to the existing placeholder behavior.
174
+ */
175
+ async function createSsrFetcher(cwd, manifest) {
176
+ try {
177
+ const mod = await import(pathToFileURL(resolve$1(cwd, manifest.serverDir, "entry.mjs")).href);
178
+ const createFetchHandler = mod.default ?? mod.createFetchHandler;
179
+ if (typeof createFetchHandler !== "function") {
180
+ logger$7.warn("SSR entry does not export createFetchHandler; falling back to placeholder prerender.");
181
+ return;
182
+ }
183
+ const fetch = await createFetchHandler();
184
+ if (typeof fetch !== "function") {
185
+ logger$7.warn("SSR entry did not return a fetch handler; falling back to placeholder prerender.");
186
+ return;
187
+ }
188
+ return async (url) => {
189
+ const req = new Request(`http://localhost${url || "/"}`, { headers: { "x-ubean-prerender": "1" } });
190
+ const res = await fetch(req);
191
+ return {
192
+ html: typeof res.text === "function" ? await res.text() : String(res.body ?? ""),
193
+ statusCode: res.status ?? 200
194
+ };
195
+ };
196
+ } catch (err) {
197
+ logger$7.warn(`Failed to load SSR entry for prerender: ${err instanceof Error ? err.message : String(err)}. Falling back to placeholder prerender.`);
198
+ return;
199
+ }
200
+ }
201
+ const buildCommand = {
202
+ meta: {
203
+ name: "build",
204
+ description: "Build the ubean application for production"
205
+ },
206
+ args: {
207
+ preset: {
208
+ type: "string",
209
+ description: "Deployment preset (node, bun, deno, cloudflare, vercel, netlify, standard)"
210
+ },
211
+ mode: {
212
+ type: "string",
213
+ description: "App mode (fullstack, spa, ssg, backend). Overrides ubean.config.ts mode"
214
+ },
215
+ ssr: {
216
+ type: "boolean",
217
+ description: "Enable SSR in fullstack mode (only effective with --mode fullstack). Use --no-ssr to disable"
218
+ },
219
+ ssg: {
220
+ type: "boolean",
221
+ description: "Shortcut for --mode ssg",
222
+ default: false
223
+ },
224
+ minify: {
225
+ type: "boolean",
226
+ description: "Minify output",
227
+ default: true
228
+ },
229
+ sourcemap: {
230
+ type: "boolean",
231
+ description: "Generate sourcemaps",
232
+ default: false
233
+ },
234
+ prerender: {
235
+ type: "boolean",
236
+ description: "Pre-render static pages (SSG)"
237
+ },
238
+ cwd: {
239
+ type: "string",
240
+ description: "Project root directory",
241
+ default: "."
242
+ }
243
+ },
244
+ async run({ args }) {
245
+ const cwd = resolve$1(args.cwd || process.cwd());
246
+ logger$7.start("Building ubean application...");
247
+ try {
248
+ registerBuiltinPresets();
249
+ const config = await loadUbeanConfig(cwd);
250
+ if (args.ssg) config.mode = "ssg";
251
+ else if (args.mode) config.mode = args.mode;
252
+ if (config.mode === "fullstack" && args.ssr !== void 0) config.ssr = args.ssr;
253
+ if (config.mode === "ssg") {
254
+ if (!config.prerender.all && config.prerender.include.length === 0) config.prerender = resolvePrerenderConfig({ all: true });
255
+ } else if (config.mode === "spa" || config.mode === "backend") config.prerender = resolvePrerenderConfig();
256
+ else if (config.mode === "fullstack" && !config.ssr) config.prerender = resolvePrerenderConfig();
257
+ if (args.prerender === true) {
258
+ if (!config.prerender.enabled) config.prerender = resolvePrerenderConfig({ all: true });
259
+ } else if (args.prerender === false) config.prerender = resolvePrerenderConfig();
260
+ const preset = resolvePresetByName(args.preset || config.build.preset);
261
+ logger$7.info(`Using preset: ${preset.name}`);
262
+ logger$7.info(`App mode: ${config.mode}${config.mode === "fullstack" ? ` (ssr=${config.ssr})` : ""}`);
263
+ logger$7.info("Scanning project...");
264
+ const result = await scanProject({
265
+ cwd,
266
+ srcDir: config.srcDir,
267
+ dirs: config.dir,
268
+ ignore: config.scanOptions?.ignore
269
+ });
270
+ logger$7.info(`Found ${result.apiRoutes.length} API routes, ${result.pages.length} pages, ${result.layouts.length} layouts`);
271
+ logger$7.info("Generating types...");
272
+ await generateTypes(result, {
273
+ cwd,
274
+ srcDir: config.srcDir,
275
+ buildDir: ".ubean",
276
+ dirs: config.dir,
277
+ imports: config.imports,
278
+ components: config.components,
279
+ composablesDirs: config.imports.dirs,
280
+ componentsDirs: config.components.dirs
281
+ });
282
+ const resolvedPreset = preset;
283
+ if (resolvedPreset.hooks?.["build:before"]) await resolvedPreset.hooks["build:before"]({
284
+ cwd,
285
+ outputDir: config.build.outputDir,
286
+ preset: resolvedPreset,
287
+ config
288
+ });
289
+ logger$7.info("Building with Vite...");
290
+ const manifest = await buildProduction({
291
+ cwd,
292
+ config,
293
+ preset: resolvedPreset,
294
+ scanResult: result,
295
+ minify: args.minify,
296
+ sourcemap: args.sourcemap
297
+ });
298
+ if (config.prerender.enabled) {
299
+ logger$7.info("Prerendering static pages...");
300
+ const fetcher = await createSsrFetcher(cwd, manifest);
301
+ await prerender({
302
+ cwd,
303
+ outputDir: config.build.outputDir,
304
+ pages: result.pages,
305
+ prerender: config.prerender,
306
+ fetcher
307
+ });
308
+ }
309
+ if (config.mode === "ssg") {
310
+ const serverDir = join$1(cwd, config.build.outputDir, "server");
311
+ if (existsSync(serverDir)) {
312
+ logger$7.info("Cleaning temporary SSR bundle (SSG mode)...");
313
+ await rm(serverDir, {
314
+ recursive: true,
315
+ force: true
316
+ });
317
+ }
318
+ }
319
+ if (resolvedPreset.hooks?.["build:after"]) await resolvedPreset.hooks["build:after"]({
320
+ cwd,
321
+ outputDir: config.build.outputDir,
322
+ preset: resolvedPreset,
323
+ config,
324
+ manifest
325
+ });
326
+ logger$7.success(`Build complete for preset "${resolvedPreset.name}"!`);
327
+ logger$7.info(` Output directory: ${resolve$1(cwd, config.build.outputDir)}`);
328
+ if (manifest.entry) logger$7.info(` Server entry: ${manifest.entry}`);
329
+ logger$7.info(` Client assets: ${manifest.assets.length} files`);
330
+ } catch (err) {
331
+ logger$7.error(err instanceof Error ? err.message : String(err));
332
+ process.exit(1);
333
+ }
334
+ process.exit(0);
335
+ }
336
+ };
337
+ //#endregion
338
+ //#region src/dev.ts
339
+ const logger$6 = consola.withTag("ubean-cli");
340
+ /**
341
+ * Best-effort loader for the optional `@ubean/devtools` peer dependency.
342
+ *
343
+ * `getCustomTabs()` returns user-registered DevTools tabs (registered via
344
+ * `defineDevToolsTab`). `@ubean/devtools` is an optional peer dep — when not
345
+ * installed, fall back to an empty list. Future versions of `@ubean/devtools`
346
+ * may expose `getCustomTabs` from its main entry; we try dynamic import on
347
+ * startup and replace the placeholder if the export is available.
348
+ */
349
+ let _customTabsGetter = () => [];
350
+ import("@ubean/devtools").then((mod) => {
351
+ if (mod && typeof mod.getCustomTabs === "function") _customTabsGetter = mod.getCustomTabs;
352
+ }).catch(() => {});
353
+ function getCustomTabs() {
354
+ return _customTabsGetter();
355
+ }
356
+ const devCommand = {
357
+ meta: {
358
+ name: "dev",
359
+ description: "Start the ubean development server"
360
+ },
361
+ args: {
362
+ port: {
363
+ type: "string",
364
+ description: "Port to listen on",
365
+ default: "9527"
366
+ },
367
+ host: {
368
+ type: "string",
369
+ description: "Host to listen on",
370
+ default: "localhost"
371
+ },
372
+ strictPort: {
373
+ type: "boolean",
374
+ description: "Exit if the port is already in use, instead of auto-incrementing",
375
+ default: false
376
+ },
377
+ open: {
378
+ type: "boolean",
379
+ description: "Open browser on startup",
380
+ default: false
381
+ },
382
+ cwd: {
383
+ type: "string",
384
+ description: "Project root directory",
385
+ default: "."
386
+ }
387
+ },
388
+ async run({ args }) {
389
+ const cwd = resolve$1(args.cwd || process.cwd());
390
+ logger$6.start("Starting ubean dev server...");
391
+ registerBuiltinPresets();
392
+ const config = await loadUbeanConfig(cwd);
393
+ const port = Number(args.port) || config.dev.port;
394
+ const host = args.host || config.dev.host;
395
+ logger$6.info(`Root directory: ${config.rootDir}`);
396
+ logger$6.info(`Source directory: ${config.srcDir}`);
397
+ const preset = resolvePresetByName(config.build.preset);
398
+ logger$6.info(`Preset: ${preset.name}`);
399
+ const capabilities = createCapabilitySet(preset.capabilities || {});
400
+ logger$6.info("Running capability diagnostics...");
401
+ const diagnostics = diagnoseCapabilities(preset.name, preset.capabilities || {}, NODE_REQUIREMENTS);
402
+ logDiagnostics(diagnostics);
403
+ if (!diagnostics.valid) logger$6.warn("Some capability requirements are not met. Dev server may not function correctly.");
404
+ let { app: currentApp, layouts: currentLayouts, scanResult: currentScanResult } = await buildApp(cwd, config);
405
+ let rescanInProgress = false;
406
+ async function rescan() {
407
+ if (rescanInProgress) return;
408
+ rescanInProgress = true;
409
+ try {
410
+ const { app: newApp, layouts: newLayouts, scanResult } = await buildApp(cwd, config);
411
+ currentApp = newApp;
412
+ currentLayouts = newLayouts;
413
+ currentScanResult = scanResult;
414
+ runner.updateApp(currentApp, currentLayouts);
415
+ await runner.reload();
416
+ } catch (err) {
417
+ logger$6.error(`Rescan failed: ${err instanceof Error ? err.message : String(err)}`);
418
+ } finally {
419
+ rescanInProgress = false;
420
+ }
421
+ }
422
+ const runner = await createDevRunner({
423
+ cwd,
424
+ srcDir: config.srcDir,
425
+ port,
426
+ host,
427
+ strictPort: args.strictPort,
428
+ preset,
429
+ config,
430
+ capabilities,
431
+ app: currentApp,
432
+ layouts: currentLayouts,
433
+ devtools: {
434
+ getScanResult: () => currentScanResult,
435
+ getConfigMeta: () => ({
436
+ preset: config.build.preset,
437
+ rootDir: config.rootDir,
438
+ srcDir: config.srcDir,
439
+ openAPI: {
440
+ enabled: true,
441
+ scalarPath: "/_scalar",
442
+ openAPIPath: "/_openapi.json"
443
+ },
444
+ dir: config.dir
445
+ }),
446
+ getCustomTabs: () => getCustomTabs(),
447
+ triggerRescan: () => rescan()
448
+ },
449
+ onListen({ url }) {
450
+ const label = (text) => dim(text);
451
+ const lines = [
452
+ `${green(bold("🚀 ubean dev server ready"))}\n`,
453
+ ` → ${label("Local:")} ${cyan(url)}`,
454
+ ` → ${label("Scalar UI:")} ${cyan(`${url}/_scalar`)}`,
455
+ ` → ${label("OpenAPI:")} ${cyan(`${url}/_openapi.json`)}`
456
+ ];
457
+ if (config.devtools.enabled) lines.push(` → ${label("DevTools:")} ${cyan(`${url}${config.devtools.route}`)}`);
458
+ lines.push(` → ${dim("Press Ctrl+C to stop")}`);
459
+ logger$6.box(lines.join("\n"));
460
+ generateOpenApiTypesFromServer(url, { outDir: resolve$1(cwd, ".ubean") }).then((filePath) => {
461
+ logger$6.success(`OpenAPI types generated: ${filePath}`);
462
+ }).catch((err) => {
463
+ logger$6.warn(`Failed to generate OpenAPI types: ${err instanceof Error ? err.message : String(err)}`);
464
+ });
465
+ },
466
+ onBeforeReload() {
467
+ logger$6.info("Reloading...");
468
+ },
469
+ onAfterReload() {
470
+ logger$6.success("Reloaded");
471
+ }
472
+ });
473
+ try {
474
+ await runner.start();
475
+ } catch (err) {
476
+ logger$6.error(err?.message || String(err));
477
+ process.exit(1);
478
+ }
479
+ const watcher = createDevWatcher({
480
+ cwd,
481
+ dirs: [
482
+ "api",
483
+ "pages",
484
+ "middleware",
485
+ "layouts",
486
+ "plugins",
487
+ "app",
488
+ "routes"
489
+ ].map((d) => `${config.srcDir}/${d}`),
490
+ ignore: [
491
+ "**/node_modules/**",
492
+ "**/.git/**",
493
+ "**/.ubean/**"
494
+ ],
495
+ debounceMs: 150,
496
+ async onChange(events) {
497
+ const relevantEvents = events.filter((e) => /\.(ts|js|vue|mjs|cjs|json)$/.test(e.relativePath) && !e.relativePath.includes(".bak"));
498
+ if (relevantEvents.length === 0) return;
499
+ logger$6.info(`File change detected: ${relevantEvents[0].relativePath}`);
500
+ await rescan();
501
+ }
502
+ });
503
+ watcher.start();
504
+ const cleanup = async () => {
505
+ logger$6.info("\nShutting down...");
506
+ watcher.stop();
507
+ await runner.stop();
508
+ process.exit(0);
509
+ };
510
+ process.on("SIGINT", cleanup);
511
+ process.on("SIGTERM", cleanup);
512
+ }
513
+ };
514
+ async function buildApp(cwd, config) {
515
+ logger$6.info("Scanning project files...");
516
+ const result = await scanProject({
517
+ cwd,
518
+ srcDir: config.srcDir,
519
+ dirs: config.dir,
520
+ ignore: config.scanOptions?.ignore
521
+ });
522
+ logger$6.info(`Found ${result.apiRoutes.length} API routes, ${result.pages.length} pages, ${result.layouts.length} layouts, ${result.middlewares.length} middlewares, ${result.plugins.length} plugins`);
523
+ logger$6.info("Generating type definitions...");
524
+ await generateTypes(result, {
525
+ cwd,
526
+ srcDir: config.srcDir,
527
+ buildDir: ".ubean",
528
+ dirs: config.dir,
529
+ imports: config.imports,
530
+ components: config.components,
531
+ composablesDirs: config.imports.dirs,
532
+ componentsDirs: config.components.dirs
533
+ });
534
+ const app = createUbeanApp({
535
+ rootDir: cwd,
536
+ routes: result.apiRoutes,
537
+ middleware: result.middlewares,
538
+ pages: result.pages,
539
+ crons: result.crons,
540
+ routeRules: config.routeRules || {},
541
+ publicDir: config.dir.public,
542
+ openAPI: {
543
+ title: "UBEAN Dev API",
544
+ scalarPath: "/_scalar",
545
+ openAPIPath: "/_openapi.json"
546
+ },
547
+ i18nConfig: config.i18n
548
+ });
549
+ app.hooks.hook("request:start", (c) => {
550
+ logger$6.log(`${c.req.method} ${c.req.path}`);
551
+ });
552
+ return {
553
+ app,
554
+ layouts: result.layouts,
555
+ scanResult: result
556
+ };
557
+ }
558
+ //#endregion
559
+ //#region src/devtools.ts
560
+ const logger$5 = consola.withTag("ubean-cli");
561
+ const devtoolsCommand = {
562
+ meta: {
563
+ name: "devtools",
564
+ description: "Manage DevTools configuration and information"
565
+ },
566
+ subCommands: {
567
+ info: {
568
+ meta: {
569
+ name: "info",
570
+ description: "Show DevTools information"
571
+ },
572
+ async run() {
573
+ logger$5.info("DevTools Information");
574
+ logger$5.log("");
575
+ logger$5.log(" DevTools is built into ubean and available in dev mode.");
576
+ logger$5.log("");
577
+ logger$5.log(" Access:");
578
+ logger$5.log(" - Start dev server: pnpm dev");
579
+ logger$5.log(" - Open DevTools: Press Shift+Alt+D in the browser, or visit /__ubean/devtools/");
580
+ logger$5.log("");
581
+ logger$5.log(" Configuration (ubean.config.ts):");
582
+ logger$5.log(" devtools: {");
583
+ logger$5.log(" enabled: true, // Enable/disable DevTools (dev only)");
584
+ logger$5.log(" }");
585
+ logger$5.log("");
586
+ logger$5.log(" Features:");
587
+ logger$5.log(" - Routes: View all registered routes and their handlers");
588
+ logger$5.log(" - Pages: Inspect page components and layouts");
589
+ logger$5.log(" - APIs: Browse API routes with OpenAPI integration");
590
+ logger$5.log(" - Middleware: View registered middleware");
591
+ logger$5.log(" - Crons: See scheduled tasks and their status");
592
+ logger$5.log(" - Config: View resolved configuration");
593
+ logger$5.log(" - Plugins: Browse installed plugins and their tabs");
594
+ logger$5.log(" - CRUD: Create/delete pages, APIs, layouts, middleware, crons");
595
+ }
596
+ },
597
+ path: {
598
+ meta: {
599
+ name: "path",
600
+ description: "Print the DevTools URL path"
601
+ },
602
+ args: { port: {
603
+ type: "string",
604
+ description: "Dev server port",
605
+ default: "9527"
606
+ } },
607
+ async run({ args }) {
608
+ const port = args.port;
609
+ logger$5.log(`http://localhost:${port}/__ubean/devtools/`);
610
+ }
611
+ }
612
+ }
613
+ };
614
+ //#endregion
615
+ //#region src/env.ts
616
+ const logger$4 = consola.withTag("ubean-cli");
617
+ function parseEnvContent(content) {
618
+ const vars = [];
619
+ const lines = content.split("\n");
620
+ for (const line of lines) {
621
+ const trimmed = line.trim();
622
+ if (!trimmed || trimmed.startsWith("#")) continue;
623
+ const eqIndex = trimmed.indexOf("=");
624
+ if (eqIndex === -1) continue;
625
+ const key = trimmed.slice(0, eqIndex).trim();
626
+ let value = trimmed.slice(eqIndex + 1).trim();
627
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
628
+ const isPublic = key.startsWith("UBEAN_PUBLIC_") || key.startsWith("VITE_") || key.startsWith("PUBLIC_");
629
+ vars.push({
630
+ key,
631
+ value,
632
+ public: isPublic
633
+ });
634
+ }
635
+ return vars;
636
+ }
637
+ function serializeEnvVars(vars) {
638
+ return `${vars.map((v) => `${v.key}=${v.value}`).join("\n")}\n`;
639
+ }
640
+ async function readEnvFile(fs, file) {
641
+ try {
642
+ return parseEnvContent(await fs.readFile(file));
643
+ } catch {
644
+ return [];
645
+ }
646
+ }
647
+ async function writeEnvFile(fs, file, vars, dry = false) {
648
+ if (dry) return true;
649
+ const content = serializeEnvVars(vars);
650
+ await fs.writeFile(file, content);
651
+ return true;
652
+ }
653
+ const envCommand = {
654
+ meta: {
655
+ name: "env",
656
+ description: "Manage environment variables"
657
+ },
658
+ subCommands: {
659
+ add: {
660
+ meta: {
661
+ name: "add",
662
+ description: "Add or update an environment variable"
663
+ },
664
+ args: {
665
+ key: {
666
+ type: "positional",
667
+ description: "Variable name (e.g., DATABASE_URL, UBEAN_PUBLIC_API_URL)"
668
+ },
669
+ value: {
670
+ type: "positional",
671
+ description: "Variable value"
672
+ },
673
+ file: {
674
+ type: "string",
675
+ description: "Environment file",
676
+ default: ".env"
677
+ },
678
+ public: {
679
+ type: "boolean",
680
+ description: "Mark as public (prefix with UBEAN_PUBLIC_)",
681
+ default: false
682
+ },
683
+ force: {
684
+ type: "boolean",
685
+ description: "Overwrite existing variable without confirmation",
686
+ default: false,
687
+ alias: "f"
688
+ },
689
+ dry: {
690
+ type: "boolean",
691
+ description: "Show what would be changed",
692
+ default: false
693
+ }
694
+ },
695
+ async run({ args }) {
696
+ const fs = createFsOps(process.cwd());
697
+ const file = args.file;
698
+ let key = args.key;
699
+ const value = args.value;
700
+ if (args.public && !key.startsWith("UBEAN_PUBLIC_") && !key.startsWith("VITE_") && !key.startsWith("PUBLIC_")) key = `UBEAN_PUBLIC_${key}`;
701
+ const vars = await readEnvFile(fs, file);
702
+ const existing = vars.find((v) => v.key === key);
703
+ if (existing && !args.force) {
704
+ logger$4.warn(`Variable ${key} already exists in ${file} (use --force to overwrite)`);
705
+ logger$4.info(` Current value: ${existing.value}`);
706
+ return;
707
+ }
708
+ if (existing) existing.value = value;
709
+ else vars.push({
710
+ key,
711
+ value,
712
+ public: key.startsWith("UBEAN_PUBLIC_") || key.startsWith("VITE_")
713
+ });
714
+ await writeEnvFile(fs, file, vars, args.dry);
715
+ if (args.dry) logger$4.success(`[dry-run] Would ${existing ? "update" : "add"} ${key}=${value} in ${file}`);
716
+ else logger$4.success(`${existing ? "Updated" : "Added"} ${key} in ${file}`);
717
+ if (key.startsWith("UBEAN_PUBLIC_") || key.startsWith("VITE_") || key.startsWith("PUBLIC_")) logger$4.info(` Note: ${key} is exposed to client-side code`);
718
+ }
719
+ },
720
+ remove: {
721
+ meta: {
722
+ name: "remove",
723
+ description: "Remove an environment variable"
724
+ },
725
+ args: {
726
+ key: {
727
+ type: "positional",
728
+ description: "Variable name to remove"
729
+ },
730
+ file: {
731
+ type: "string",
732
+ description: "Environment file",
733
+ default: ".env"
734
+ },
735
+ dry: {
736
+ type: "boolean",
737
+ description: "Show what would be removed",
738
+ default: false
739
+ }
740
+ },
741
+ async run({ args }) {
742
+ const fs = createFsOps(process.cwd());
743
+ const file = args.file;
744
+ const key = args.key;
745
+ const vars = await readEnvFile(fs, file);
746
+ const index = vars.findIndex((v) => v.key === key);
747
+ if (index === -1) {
748
+ logger$4.warn(`Variable ${key} not found in ${file}`);
749
+ return;
750
+ }
751
+ vars.splice(index, 1);
752
+ if (args.dry) logger$4.success(`[dry-run] Would remove ${key} from ${file}`);
753
+ else {
754
+ await writeEnvFile(fs, file, vars);
755
+ logger$4.success(`Removed ${key} from ${file}`);
756
+ }
757
+ }
758
+ },
759
+ list: {
760
+ meta: {
761
+ name: "list",
762
+ description: "List environment variables"
763
+ },
764
+ args: {
765
+ file: {
766
+ type: "string",
767
+ description: "Environment file",
768
+ default: ".env"
769
+ },
770
+ public: {
771
+ type: "boolean",
772
+ description: "Show only public variables",
773
+ default: false
774
+ }
775
+ },
776
+ async run({ args }) {
777
+ const fs = createFsOps(process.cwd());
778
+ const file = args.file;
779
+ if (!await fs.exists(file)) {
780
+ logger$4.info(`${file} does not exist`);
781
+ return;
782
+ }
783
+ const vars = await readEnvFile(fs, file);
784
+ const filtered = args.public ? vars.filter((v) => v.public) : vars;
785
+ if (filtered.length === 0) {
786
+ logger$4.info(`No variables found in ${file}${args.public ? " (public only)" : ""}`);
787
+ return;
788
+ }
789
+ logger$4.info(`Variables in ${file} (${filtered.length}):`);
790
+ for (const v of filtered) {
791
+ const suffix = v.public ? " [public]" : "";
792
+ logger$4.log(` ${v.key}=${v.value}${suffix}`);
793
+ }
794
+ const publicCount = vars.filter((v) => v.public).length;
795
+ const privateCount = vars.length - publicCount;
796
+ logger$4.info(`\n ${publicCount} public, ${privateCount} private`);
797
+ }
798
+ },
799
+ init: {
800
+ meta: {
801
+ name: "init",
802
+ description: "Create .env and .env.example files from template"
803
+ },
804
+ args: {
805
+ force: {
806
+ type: "boolean",
807
+ description: "Overwrite existing files",
808
+ default: false,
809
+ alias: "f"
810
+ },
811
+ dry: {
812
+ type: "boolean",
813
+ description: "Show what would be created",
814
+ default: false
815
+ }
816
+ },
817
+ async run({ args }) {
818
+ const fs = createFsOps(process.cwd());
819
+ for (const [file, content] of [[".env", `# ubean environment variables
820
+ # Server-only variables (never exposed to client)
821
+ DATABASE_URL=
822
+ SESSION_SECRET=
823
+
824
+ # Public variables (exposed to client - prefix with UBEAN_PUBLIC_)
825
+ # UBEAN_PUBLIC_API_URL=/api
826
+ `], [".env.example", `# Example environment variables
827
+ DATABASE_URL=your-database-url
828
+ SESSION_SECRET=your-session-secret
829
+ # UBEAN_PUBLIC_API_URL=/api
830
+ `]]) {
831
+ if (await fs.exists(file) && !args.force) {
832
+ logger$4.warn(`Skipping ${file} (already exists, use --force to overwrite)`);
833
+ continue;
834
+ }
835
+ if (args.dry) logger$4.success(`[dry-run] Would create ${file}`);
836
+ else {
837
+ await fs.writeFile(file, content);
838
+ logger$4.success(`Created ${file}`);
839
+ }
840
+ }
841
+ logger$4.info("\nTip: Add .env to .gitignore (but commit .env.example)");
842
+ }
843
+ }
844
+ }
845
+ };
846
+ //#endregion
847
+ //#region src/init.ts
848
+ const logger$3 = consola.withTag("ubean-cli");
849
+ const TEMPLATES = [
850
+ {
851
+ value: "minimal",
852
+ label: "Minimal (just a hello world page)"
853
+ },
854
+ {
855
+ value: "starter",
856
+ label: "Starter (recommended, includes pages/api/layouts)"
857
+ },
858
+ {
859
+ value: "blog",
860
+ label: "Blog (markdown-based blog structure)"
861
+ }
862
+ ];
863
+ const PRESETS = [
864
+ {
865
+ value: "standard",
866
+ label: "Standard (Node.js/Bun compatible)"
867
+ },
868
+ {
869
+ value: "node",
870
+ label: "Node.js"
871
+ },
872
+ {
873
+ value: "cloudflare",
874
+ label: "Cloudflare Workers"
875
+ }
876
+ ];
877
+ const PACKAGE_MANAGERS = [
878
+ "npm",
879
+ "pnpm",
880
+ "yarn",
881
+ "bun"
882
+ ];
883
+ async function prompt(question, defaultValue) {
884
+ const rl = (await import("node:readline")).createInterface({
885
+ input: process.stdin,
886
+ output: process.stdout
887
+ });
888
+ return new Promise((resolvePromise) => {
889
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
890
+ rl.question(`${question}${suffix}: `, (answer) => {
891
+ rl.close();
892
+ resolvePromise(answer.trim() || defaultValue || "");
893
+ });
894
+ });
895
+ }
896
+ async function select(question, options, defaultValue) {
897
+ const rl = (await import("node:readline")).createInterface({
898
+ input: process.stdin,
899
+ output: process.stdout
900
+ });
901
+ console.log(`\n${question}`);
902
+ options.forEach((opt, i) => {
903
+ const marker = opt.value === defaultValue ? " (default)" : "";
904
+ console.log(` ${i + 1}. ${opt.label}${marker} [${opt.value}]`);
905
+ });
906
+ return new Promise((resolvePromise) => {
907
+ rl.question("> ", (answer) => {
908
+ rl.close();
909
+ const idx = parseInt(answer, 10) - 1;
910
+ if (!answer.trim() && defaultValue) resolvePromise(defaultValue);
911
+ else if (idx >= 0 && idx < options.length) resolvePromise(options[idx].value);
912
+ else {
913
+ const found = options.find((o) => o.value === answer.trim());
914
+ resolvePromise(found ? found.value : defaultValue || options[0].value);
915
+ }
916
+ });
917
+ });
918
+ }
919
+ async function confirm(question, defaultValue = true) {
920
+ const rl = (await import("node:readline")).createInterface({
921
+ input: process.stdin,
922
+ output: process.stdout
923
+ });
924
+ const suffix = defaultValue ? " (Y/n)" : " (y/N)";
925
+ return new Promise((resolvePromise) => {
926
+ rl.question(`${question}${suffix}: `, (answer) => {
927
+ rl.close();
928
+ const a = answer.trim().toLowerCase();
929
+ if (!a) resolvePromise(defaultValue);
930
+ else resolvePromise(a === "y" || a === "yes");
931
+ });
932
+ });
933
+ }
934
+ function isNonInteractive() {
935
+ return !process.stdin.isTTY || process.env.CI !== void 0 || process.env.NONINTERACTIVE !== void 0;
936
+ }
937
+ const PACKAGE_JSON_TEMPLATE = `{
938
+ "name": "{{name}}",
939
+ "private": true,
940
+ "type": "module",
941
+ "scripts": {
942
+ "dev": "ubean dev",
943
+ "build": "ubean build",
944
+ "preview": "ubean preview",
945
+ "prepare": "ubean prepare",
946
+ "typecheck": "vue-tsc --noEmit"
947
+ },
948
+ "dependencies": {
949
+ "ubean": "latest",
950
+ "vue": "^3.5.0"
951
+ },
952
+ "devDependencies": {
953
+ "typescript": "^5.6.0",
954
+ "vue-tsc": "^2.1.0"
955
+ }
956
+ }
957
+ `;
958
+ const TSCONFIG_TEMPLATE = `{
959
+ "compilerOptions": {
960
+ "target": "ESNext",
961
+ "module": "ESNext",
962
+ "moduleResolution": "Bundler",
963
+ "strict": true,
964
+ "jsx": "preserve",
965
+ "resolveJsonModule": true,
966
+ "esModuleInterop": true,
967
+ "skipLibCheck": true,
968
+ "isolatedModules": true,
969
+ "allowImportingTsExtensions": true,
970
+ "noEmit": true,
971
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
972
+ "types": ["ubean/client"]
973
+ },
974
+ "include": ["src/**/*.ts", "src/**/*.vue", ".ubean/**/*.d.ts"],
975
+ "exclude": ["node_modules", "dist"]
976
+ }
977
+ `;
978
+ const UBEAN_CONFIG_TEMPLATE = `import { defineConfig } from 'ubean';
979
+
980
+ export default defineConfig({
981
+ srcDir: 'src',
982
+ preset: '{{preset}}'
983
+ });
984
+ `;
985
+ const APP_VUE_TEMPLATE = `<template>
986
+ <div id="app">
987
+ <RouterView />
988
+ </div>
989
+ </template>
990
+
991
+ <script setup lang="ts">
992
+ import { RouterView } from 'vue-router';
993
+ <\/script>
994
+
995
+ <style>
996
+ * {
997
+ margin: 0;
998
+ padding: 0;
999
+ box-sizing: border-box;
1000
+ }
1001
+
1002
+ body {
1003
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
1004
+ }
1005
+
1006
+ #app {
1007
+ min-height: 100vh;
1008
+ }
1009
+ </style>
1010
+ `;
1011
+ const INDEX_PAGE_TEMPLATE = `<template>
1012
+ <div class="home-page">
1013
+ <h1>Welcome to ubean</h1>
1014
+ <p>Your Vue meta framework is ready! 🚀</p>
1015
+ <div class="links">
1016
+ <RouterLink to="/about">About</RouterLink>
1017
+ </div>
1018
+ </div>
1019
+ </template>
1020
+
1021
+ <script setup lang="ts">
1022
+ import { RouterLink } from 'vue-router';
1023
+
1024
+ definePage({
1025
+ meta: {
1026
+ title: 'Home'
1027
+ }
1028
+ });
1029
+ <\/script>
1030
+
1031
+ <style scoped>
1032
+ .home-page {
1033
+ display: flex;
1034
+ flex-direction: column;
1035
+ align-items: center;
1036
+ justify-content: center;
1037
+ min-height: 100vh;
1038
+ padding: 2rem;
1039
+ text-align: center;
1040
+ }
1041
+
1042
+ h1 {
1043
+ font-size: 3rem;
1044
+ margin-bottom: 1rem;
1045
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1046
+ -webkit-background-clip: text;
1047
+ -webkit-text-fill-color: transparent;
1048
+ background-clip: text;
1049
+ }
1050
+
1051
+ p {
1052
+ color: #666;
1053
+ font-size: 1.2rem;
1054
+ margin-bottom: 2rem;
1055
+ }
1056
+
1057
+ .links {
1058
+ display: flex;
1059
+ gap: 1rem;
1060
+ }
1061
+
1062
+ a {
1063
+ padding: 0.75rem 1.5rem;
1064
+ border-radius: 8px;
1065
+ background: #667eea;
1066
+ color: white;
1067
+ text-decoration: none;
1068
+ font-weight: 500;
1069
+ transition: all 0.2s;
1070
+ }
1071
+
1072
+ a:hover {
1073
+ background: #5a67d8;
1074
+ transform: translateY(-1px);
1075
+ }
1076
+ </style>
1077
+ `;
1078
+ const ABOUT_PAGE_TEMPLATE = `<template>
1079
+ <div class="about-page">
1080
+ <h1>About</h1>
1081
+ <p>This page is built with ubean.</p>
1082
+ <RouterLink to="/">Back to Home</RouterLink>
1083
+ </div>
1084
+ </template>
1085
+
1086
+ <script setup lang="ts">
1087
+ import { RouterLink } from 'vue-router';
1088
+
1089
+ definePage({
1090
+ meta: {
1091
+ title: 'About'
1092
+ }
1093
+ });
1094
+ <\/script>
1095
+
1096
+ <style scoped>
1097
+ .about-page {
1098
+ max-width: 800px;
1099
+ margin: 0 auto;
1100
+ padding: 3rem 2rem;
1101
+ }
1102
+
1103
+ h1 {
1104
+ margin-bottom: 1rem;
1105
+ color: #333;
1106
+ }
1107
+
1108
+ p {
1109
+ color: #666;
1110
+ line-height: 1.6;
1111
+ margin-bottom: 2rem;
1112
+ }
1113
+
1114
+ a {
1115
+ color: #667eea;
1116
+ text-decoration: none;
1117
+ }
1118
+
1119
+ a:hover {
1120
+ text-decoration: underline;
1121
+ }
1122
+ </style>
1123
+ `;
1124
+ const HELLO_API_TEMPLATE = `import { defineHandler } from 'ubean';
1125
+
1126
+ export const GET = defineHandler(c => {
1127
+ return c.json({
1128
+ message: 'Hello from ubean API!',
1129
+ timestamp: Date.now()
1130
+ });
1131
+ });
1132
+ `;
1133
+ /**
1134
+ * 类型化请求客户端模板 - 浏览器端
1135
+ *
1136
+ * 启动 dev server 后,ubean 会自动生成 .ubean/openapi.d.ts(包含项目所有 API 的 paths 类型)。
1137
+ * 本文件将 paths 类型绑定到 createTypedClient,使浏览器端请求的路径、参数、请求体和返回值都获得类型安全。
1138
+ *
1139
+ * 使用方式:
1140
+ * import { api } from '../request/client';
1141
+ * const user = await api.get('/api/users/{id}', { params: { path: { id: 1 } } });
1142
+ */
1143
+ const REQUEST_CLIENT_TEMPLATE = `import { createRequest } from '@soybeanjs/fetch';
1144
+ import { createTypedClient, toFlatTypedClient } from '@soybeanjs/fetch/openapi';
1145
+ import type { paths } from '../../.ubean/openapi';
1146
+
1147
+ /**
1148
+ * 底层 HTTP 请求实例(@soybeanjs/fetch 封装)。
1149
+ *
1150
+ * api 和 flatApi 共用同一份 RequestInstance,共享 baseURL/timeout/headers/state 等。
1151
+ * 调整配置时只需修改此处一处。
1152
+ */
1153
+ const request = createRequest({
1154
+ // baseURL: '/api', // 按需设置 API 基础路径
1155
+ // timeout: 10000,
1156
+ });
1157
+
1158
+ /**
1159
+ * 浏览器端类型化 HTTP 客户端(抛异常模式)
1160
+ *
1161
+ * 路径、参数、请求体和返回值类型均从 OpenAPI schema 自动推断。
1162
+ * 启动 dev server 后类型会自动更新(.ubean/openapi.d.ts)。
1163
+ *
1164
+ * 支持通过 responseType 配置不同的返回类型:
1165
+ * - 'json' (默认): 返回解析后的 JSON 数据
1166
+ * - 'blob': 返回 { file: Blob; filename: string; contentType: string }
1167
+ * - 'text': 返回 string
1168
+ * - 'arraybuffer': 返回 { file: ArrayBuffer; filename: string; contentType: string }
1169
+ *
1170
+ * @example
1171
+ * import { api } from '../request/client';
1172
+ *
1173
+ * // JSON 请求(默认)
1174
+ * const user = await api.get('/api/users/{id}', { pathParams: { id: 1 } });
1175
+ *
1176
+ * // 文件下载
1177
+ * const file = await api.get('/api/export', { responseType: 'blob' });
1178
+ * // file: { file: Blob; filename: string; contentType: string }
1179
+ *
1180
+ * // 文本
1181
+ * const text = await api.get('/api/readme', { responseType: 'text' });
1182
+ * // text: string
1183
+ */
1184
+ export const api = createTypedClient<paths>(request);
1185
+
1186
+ /**
1187
+ * 扁平模式类型化 HTTP 客户端(不抛异常,通过返回值判断)
1188
+ *
1189
+ * toFlatTypedClient 接收同一个 RequestInstance,内部通过 withResponse + try/catch
1190
+ * 实现扁平化返回,与 api 共享 state/cache/auth。
1191
+ *
1192
+ * 返回 { data, error, response } — 成功时 error 为 null,失败时 data 为 null。
1193
+ * 接口与 api 一致,同样支持 responseType。
1194
+ *
1195
+ * @example
1196
+ * import { flatApi } from '../request/client';
1197
+ *
1198
+ * const { data, error } = await flatApi.get('/api/users/{id}', {
1199
+ * pathParams: { id: 1 }
1200
+ * });
1201
+ * if (error) {
1202
+ * console.error('Failed:', error.message);
1203
+ * } else {
1204
+ * console.log('User:', data);
1205
+ * }
1206
+ */
1207
+ export const flatApi = toFlatTypedClient<paths>(request);
1208
+ `;
1209
+ /**
1210
+ * 类型化请求客户端模板 - server 端内部调度
1211
+ *
1212
+ * 在 API 路由或 useData 的 fetcher 中使用,自动转发 cookie/authorization 等请求头。
1213
+ * 与 client.ts 接口一致,但内部通过 createInternalAdapter + @soybeanjs/fetch 发起请求(进程内调度)。
1214
+ *
1215
+ * 使用方式:
1216
+ * import { createServerApi } from '../request/internal';
1217
+ * export const GET = defineHandler(async (c) => {
1218
+ * const api = createServerApi(c);
1219
+ * const user = await api.get('/api/users/{id}', { pathParams: { id: 1 } });
1220
+ * return c.json(user);
1221
+ * });
1222
+ */
1223
+ const REQUEST_INTERNAL_TEMPLATE = `import { createInternalAdapter } from 'ubean';
1224
+ import { createRequest } from '@soybeanjs/fetch';
1225
+ import { createTypedClient } from '@soybeanjs/fetch/openapi';
1226
+ import type { paths } from '../../.ubean/openapi';
1227
+
1228
+ /**
1229
+ * server 端类型化内部 fetch
1230
+ *
1231
+ * 在 API 路由或 useData 的 fetcher 中使用,自动转发 cookie/authorization 等请求头。
1232
+ * 与 client.ts 的 api 接口一致,但通过进程内调度发起请求(不发起新的网络请求)。
1233
+ *
1234
+ * @example
1235
+ * // src/routes/api/posts.ts
1236
+ * import { defineHandler } from 'ubean';
1237
+ * import { createServerApi } from '../request/internal';
1238
+ *
1239
+ * export const GET = defineHandler(async (c) => {
1240
+ * const api = createServerApi(c);
1241
+ * const user = await api.get('/api/users/{id}', { pathParams: { id: 1 } });
1242
+ * return c.json(user);
1243
+ * });
1244
+ *
1245
+ * @example
1246
+ * // 在 useData 中使用
1247
+ * import { useData, defineHandler } from 'ubean';
1248
+ * import { createServerApi } from '../request/internal';
1249
+ *
1250
+ * export const GET = defineHandler(async (c) => {
1251
+ * const api = createServerApi(c);
1252
+ * const result = await useData({
1253
+ * fetcher: async () => api.get('/api/users/{id}', { pathParams: { id: 1 } })
1254
+ * });
1255
+ * return c.json(result.data);
1256
+ * });
1257
+ */
1258
+ export function createServerApi(context: Parameters<typeof createInternalAdapter>[0]) {
1259
+ const adapter = createInternalAdapter(context);
1260
+ const request = createRequest(
1261
+ { retry: { retries: 0 }, adapter },
1262
+ { isBackendSuccess: () => true, transform: response => response.data }
1263
+ );
1264
+ return createTypedClient<paths>(request);
1265
+ }
1266
+ `;
1267
+ const DEFAULT_LAYOUT_TEMPLATE = `<template>
1268
+ <slot />
1269
+ </template>
1270
+
1271
+ <script setup lang="ts">
1272
+ <\/script>
1273
+
1274
+ <style>
1275
+ body {
1276
+ line-height: 1.5;
1277
+ }
1278
+ </style>
1279
+ `;
1280
+ const GITIGNORE_TEMPLATE = `node_modules
1281
+ dist
1282
+ .ubean
1283
+ .DS_Store
1284
+ *.log
1285
+ .env
1286
+ .env.local
1287
+ .env.*.local
1288
+ `;
1289
+ const FAVICON_SVG_TEMPLATE = `<svg width="100%" height="100%" version="1.1" viewBox="0 0 1000 1000" xmlns="http://www.w3.org/2000/svg"
1290
+ xmlns:xlink="http://www.w3.org/1999/xlink">
1291
+ <g>
1292
+ <path
1293
+ d="M 200,866 C 100,866 50,779.4 100,692.8 L 200,519.6 C 220,485 240,490 265,499.6 S 360,542.68 360,542.68 C 480.5,601 498,642.5 500,720 C 498,811 462,856 420,866"
1294
+ fill="url(#LinearGradient)" fill-rule="nonzero" opacity="1" stroke="none" />
1295
+ <path
1296
+ d="M 420,866 C 455,861 478,846 500,827 C 614,696 615,597 500,517 C 394,444 333,374 380,207.82 L 260,415.67 C 240.22,450 254.37,465.1 275.28,481.79 S 360,542.68 360,542.68 C 480.5,601 498,642.5 500,720 C 498,811 462,856 420,866"
1297
+ fill="url(#LinearGradient_2)" fill-rule="nonzero" opacity="1" stroke="none" />
1298
+ <path d="M 500,517 C 394,444 333,374 380,207.82 L 400,173.2 C 367,295 421,350 603,428 C 572,440 524,474 500,517"
1299
+ fill="url(#LinearGradient_3)" fill-rule="nonzero" opacity="1" stroke="none" />
1300
+ <path d="M 500,827 L 660,660 C 738,589 710,482 603,428 C 572,440 524,474 500,517 C 615,597 614,696 500,827"
1301
+ fill="url(#LinearGradient_4)" fill-rule="nonzero" opacity="1" stroke="none" />
1302
+ <path d="M 400,173.2 C 367,295 421,350 603,428 C 690,389, 750,445 788,500 L 600,173.2 C 550,86.6 450,86.6 400,173.2"
1303
+ fill="url(#LinearGradient_5)" fill-rule="nonzero" opacity="1" stroke="none" />
1304
+ <path
1305
+ d="M 500,827 L 660,660 C 738,589 710,482 603,428 C 690,389, 750,445 788,500 C 816,554 797,606 750,640 L 500,827"
1306
+ fill="url(#LinearGradient_6)" fill-rule="nonzero" opacity="1" stroke="none" />
1307
+ <path
1308
+ d="M 788,500 C 816,554 797,606 750,640 L 500,827 C 497,851 513,862 540,866 L 800,866 C 900,866 950,779.4 900,692.8 L 788,500"
1309
+ fill="url(#LinearGradient_7)" fill-rule="nonzero" opacity="1" stroke="none" />
1310
+ <g transform="translate(140, 650) scale(0.18)">
1311
+ <path d="M539.04,146.5c-77.37,32.74-126.47,114.53-123.22,198,.61,150.92,169.47,210.6,214.99,341.13,39.71,96.62-16.62,230.68-122.31,253.16-374.89-83.15-390.44-785.49,30.54-792.29ZM564.61,948.5c68.1-32.18,118.51-95.45,132.49-169.97,41.81-214.6-158.22-254.7-215.15-413.02-31.14-86.21,21.33-198.66,114.56-214.57,129.21,21.58,214.93,148.7,230.96,272.19,7.94,38.53,21.57,75.37,34.41,112.43,75.75,198.26-76.06,438.99-297.26,412.94Z" fill="#ffffff" fill-rule="evenodd"/>
1312
+ </g>
1313
+ </g>
1314
+ <defs>
1315
+ <linearGradient gradientTransform="matrix(104.391 -73.3432 73.3432 104.391 277.441 710.122)"
1316
+ gradientUnits="userSpaceOnUse" id="LinearGradient" x1="0" x2="1" y1="0" y2="0">
1317
+ <stop offset="0" stop-color="#373ebf" />
1318
+ <stop offset="1" stop-color="#5058e6" />
1319
+ </linearGradient>
1320
+ <linearGradient gradientTransform="matrix(-173.747 557.324 -557.324 -173.747 508.829 258.172)"
1321
+ gradientUnits="userSpaceOnUse" id="LinearGradient_2" x1="0" x2="1" y1="0" y2="0">
1322
+ <stop offset="0" stop-color="#c2d6ff" />
1323
+ <stop offset="1" stop-color="#646cff" />
1324
+ </linearGradient>
1325
+ <linearGradient gradientTransform="matrix(157.951 295.666 -295.666 157.951 382.944 193.642)"
1326
+ gradientUnits="userSpaceOnUse" id="LinearGradient_3" x1="0" x2="1" y1="0" y2="0">
1327
+ <stop offset="0" stop-color="#5058e6" />
1328
+ <stop offset="1" stop-color="#373ebf" />
1329
+ </linearGradient>
1330
+ <linearGradient gradientTransform="matrix(-44.3023 219.578 -219.578 -44.3023 619.69 469.652)"
1331
+ gradientUnits="userSpaceOnUse" id="LinearGradient_4" x1="0" x2="1" y1="0" y2="0">
1332
+ <stop offset="0" stop-color="#91a7ff" />
1333
+ <stop offset="1" stop-color="#5058e6" />
1334
+ </linearGradient>
1335
+ <linearGradient gradientTransform="matrix(125.52 334.256 -334.256 125.52 539.723 235.139)"
1336
+ gradientUnits="userSpaceOnUse" id="LinearGradient_5" x1="0" x2="1" y1="0" y2="0">
1337
+ <stop offset="0" stop-color="#646cff" />
1338
+ <stop offset="1" stop-color="#c2d6ff" />
1339
+ </linearGradient>
1340
+ <linearGradient gradientTransform="matrix(-241.23 357.206 -357.206 -241.23 754.054 449.312)"
1341
+ gradientUnits="userSpaceOnUse" id="LinearGradient_6" x1="0" x2="1" y1="0" y2="0">
1342
+ <stop offset="0" stop-color="#c2d6ff" />
1343
+ <stop offset="1" stop-color="#646cff" />
1344
+ </linearGradient>
1345
+ <linearGradient gradientTransform="matrix(125.978 210.065 -210.065 125.978 596.433 613.665)"
1346
+ gradientUnits="userSpaceOnUse" id="LinearGradient_7" x1="0" x2="1" y1="0" y2="0">
1347
+ <stop offset="0" stop-color="#373ebf" />
1348
+ <stop offset="1" stop-color="#5058e6" />
1349
+ </linearGradient>
1350
+ </defs>
1351
+ </svg>
1352
+ `;
1353
+ const README_TEMPLATE = `# {{name}}
1354
+
1355
+ A Vue meta-framework project powered by ubean.
1356
+
1357
+ ## Getting Started
1358
+
1359
+ \`\`\`bash
1360
+ # Install dependencies
1361
+ {{pm}} install
1362
+
1363
+ # Start dev server
1364
+ {{pm}} dev
1365
+
1366
+ # Build for production
1367
+ {{pm}} build
1368
+
1369
+ # Preview production build
1370
+ {{pm}} preview
1371
+ \`\`\`
1372
+
1373
+ ## Project Structure
1374
+
1375
+ \`\`\`
1376
+ ├── src/
1377
+ │ ├── pages/ # File-based routing
1378
+ │ ├── layouts/ # Layout components
1379
+ │ ├── routes/ # API routes
1380
+ │ ├── components/ # Vue components
1381
+ │ ├── composables/ # Auto-imported composables
1382
+ │ ├── plugins/ # App plugins
1383
+ │ ├── middleware/ # Request middleware
1384
+ │ ├── crons/ # Scheduled tasks
1385
+ │ ├── queues/ # Background jobs
1386
+ │ ├── request/ # Typed HTTP client & internal fetch
1387
+ │ └── app.vue # Root app component
1388
+ ├── public/ # Static assets
1389
+ └── ubean.config.ts # ubean configuration
1390
+ \`\`\`
1391
+ `;
1392
+ const BLOG_INDEX_TEMPLATE = `<template>
1393
+ <div class="blog-page">
1394
+ <h1>Blog</h1>
1395
+ <div class="posts">
1396
+ <article v-for="post in posts" :key="post.slug" class="post-card">
1397
+ <h2><RouterLink :to="'/blog/' + post.slug">{{ post.title }}</RouterLink></h2>
1398
+ <p class="date">{{ post.date }}</p>
1399
+ <p class="excerpt">{{ post.excerpt }}</p>
1400
+ </article>
1401
+ </div>
1402
+ </div>
1403
+ </template>
1404
+
1405
+ <script setup lang="ts">
1406
+ import { RouterLink } from 'vue-router';
1407
+ import { useData } from 'ubean';
1408
+
1409
+ definePage({
1410
+ meta: {
1411
+ title: 'Blog'
1412
+ }
1413
+ });
1414
+
1415
+ const { data: posts } = await useData('posts', () => {
1416
+ return [
1417
+ { slug: 'hello-world', title: 'Hello World', date: '2024-01-15', excerpt: 'Welcome to my blog!' }
1418
+ ];
1419
+ });
1420
+ <\/script>
1421
+
1422
+ <style scoped>
1423
+ .blog-page {
1424
+ max-width: 800px;
1425
+ margin: 0 auto;
1426
+ padding: 3rem 2rem;
1427
+ }
1428
+
1429
+ h1 {
1430
+ margin-bottom: 2rem;
1431
+ }
1432
+
1433
+ .posts {
1434
+ display: flex;
1435
+ flex-direction: column;
1436
+ gap: 2rem;
1437
+ }
1438
+
1439
+ .post-card {
1440
+ padding: 1.5rem;
1441
+ border-radius: 8px;
1442
+ border: 1px solid #eee;
1443
+ }
1444
+
1445
+ h2 {
1446
+ font-size: 1.5rem;
1447
+ margin-bottom: 0.5rem;
1448
+ }
1449
+
1450
+ h2 a {
1451
+ color: #333;
1452
+ text-decoration: none;
1453
+ }
1454
+
1455
+ h2 a:hover {
1456
+ color: #667eea;
1457
+ }
1458
+
1459
+ .date {
1460
+ color: #999;
1461
+ font-size: 0.9rem;
1462
+ margin-bottom: 0.5rem;
1463
+ }
1464
+
1465
+ .excerpt {
1466
+ color: #666;
1467
+ }
1468
+ </style>
1469
+ `;
1470
+ const BLOG_POST_MD_TEMPLATE = `---
1471
+ title: Hello World
1472
+ date: 2024-01-15
1473
+ description: Welcome to my ubean blog!
1474
+ ---
1475
+
1476
+ # Hello World
1477
+
1478
+ Welcome to my new blog built with **ubean**!
1479
+
1480
+ This is a markdown blog post.
1481
+
1482
+ ## Features
1483
+
1484
+ - File-based routing
1485
+ - Markdown support
1486
+ - Auto-imports
1487
+ - API routes
1488
+ - And much more!
1489
+ `;
1490
+ async function scaffoldProject(options) {
1491
+ const targetDir = resolve$1(options.cwd, options.dir);
1492
+ const fs = createFsOps(targetDir);
1493
+ const projectName = options.name || (options.dir === "." ? basename(options.cwd) : basename(options.dir));
1494
+ const pm = options.packageManager || "npm";
1495
+ const nonInteractive = options.nonInteractive || isNonInteractive();
1496
+ if (await fs.exists(".") && !options.force) {
1497
+ if ((await fs.readDir(".").catch(() => [])).filter((f) => !f.startsWith(".") && f !== "node_modules").length > 0) {
1498
+ if (nonInteractive) throw new Error(`Directory ${targetDir} is not empty. Use --force to overwrite.`);
1499
+ if (!await confirm(`Directory ${targetDir} is not empty. Continue anyway?`, false)) {
1500
+ console.log("Aborted.");
1501
+ return;
1502
+ }
1503
+ }
1504
+ }
1505
+ await fs.ensureDir("src");
1506
+ await fs.ensureDir("src/pages");
1507
+ await fs.ensureDir("src/layouts");
1508
+ await fs.ensureDir("src/routes");
1509
+ await fs.ensureDir("src/components");
1510
+ await fs.ensureDir("src/composables");
1511
+ await fs.ensureDir("src/plugins");
1512
+ await fs.ensureDir("src/request");
1513
+ await fs.ensureDir("public");
1514
+ await fs.writeFile("package.json", renderTemplate(PACKAGE_JSON_TEMPLATE, { variables: { name: projectName } }));
1515
+ await fs.writeFile("public/favicon.svg", FAVICON_SVG_TEMPLATE);
1516
+ await fs.writeFile("tsconfig.json", TSCONFIG_TEMPLATE);
1517
+ await fs.writeFile("ubean.config.ts", renderTemplate(UBEAN_CONFIG_TEMPLATE, { variables: { preset: options.preset || "standard" } }));
1518
+ await fs.writeFile(".gitignore", GITIGNORE_TEMPLATE);
1519
+ await fs.writeFile("README.md", renderTemplate(README_TEMPLATE, { variables: {
1520
+ name: projectName,
1521
+ pm
1522
+ } }));
1523
+ await fs.writeFile("src/app.vue", APP_VUE_TEMPLATE);
1524
+ await fs.writeFile("src/layouts/default.vue", DEFAULT_LAYOUT_TEMPLATE);
1525
+ await fs.writeFile("src/request/client.ts", REQUEST_CLIENT_TEMPLATE);
1526
+ await fs.writeFile("src/request/internal.ts", REQUEST_INTERNAL_TEMPLATE);
1527
+ if (options.template === "minimal") await fs.writeFile("src/pages/index.vue", INDEX_PAGE_TEMPLATE);
1528
+ else if (options.template === "blog") {
1529
+ await fs.writeFile("src/pages/index.vue", INDEX_PAGE_TEMPLATE);
1530
+ await fs.writeFile("src/pages/about.vue", ABOUT_PAGE_TEMPLATE);
1531
+ await fs.ensureDir("src/pages/blog");
1532
+ await fs.writeFile("src/pages/blog/index.vue", BLOG_INDEX_TEMPLATE);
1533
+ await fs.writeFile("src/pages/blog/hello-world.md", BLOG_POST_MD_TEMPLATE);
1534
+ await fs.writeFile("src/routes/hello.get.ts", HELLO_API_TEMPLATE);
1535
+ } else {
1536
+ await fs.writeFile("src/pages/index.vue", INDEX_PAGE_TEMPLATE);
1537
+ await fs.writeFile("src/pages/about.vue", ABOUT_PAGE_TEMPLATE);
1538
+ await fs.writeFile("src/routes/hello.get.ts", HELLO_API_TEMPLATE);
1539
+ }
1540
+ if (options.git) try {
1541
+ const { execSync } = await import("node:child_process");
1542
+ execSync("git init", {
1543
+ cwd: targetDir,
1544
+ stdio: "ignore"
1545
+ });
1546
+ } catch {}
1547
+ console.log("\n✨ ubean project created successfully!\n");
1548
+ console.log(`Project: ${projectName}`);
1549
+ console.log(`Location: ${targetDir}`);
1550
+ console.log(`Preset: ${options.preset || "standard"}`);
1551
+ console.log(`Template: ${options.template || "starter"}`);
1552
+ console.log("\nNext steps:");
1553
+ if (options.dir !== ".") console.log(` cd ${options.dir}`);
1554
+ console.log(` ${pm} install`);
1555
+ console.log(` ${pm} dev`);
1556
+ console.log("");
1557
+ }
1558
+ const initCommand = {
1559
+ meta: {
1560
+ name: "init",
1561
+ description: "Initialize a new ubean project"
1562
+ },
1563
+ args: {
1564
+ dir: {
1565
+ type: "positional",
1566
+ description: "Directory to initialize",
1567
+ default: "."
1568
+ },
1569
+ force: {
1570
+ type: "boolean",
1571
+ description: "Overwrite existing files",
1572
+ default: false,
1573
+ alias: "f"
1574
+ },
1575
+ name: {
1576
+ type: "string",
1577
+ description: "Project name"
1578
+ },
1579
+ template: {
1580
+ type: "string",
1581
+ description: "Project template (minimal/starter/blog)"
1582
+ },
1583
+ preset: {
1584
+ type: "string",
1585
+ description: "Target preset (standard/node/cloudflare)"
1586
+ },
1587
+ packageManager: {
1588
+ type: "string",
1589
+ description: "Package manager (npm/pnpm/yarn/bun)",
1590
+ alias: "pm"
1591
+ },
1592
+ git: {
1593
+ type: "boolean",
1594
+ description: "Initialize git repository"
1595
+ },
1596
+ yes: {
1597
+ type: "boolean",
1598
+ description: "Skip prompts and use defaults",
1599
+ default: false,
1600
+ alias: "y"
1601
+ }
1602
+ },
1603
+ async run({ args, data }) {
1604
+ const cwd = data?.cwd || process.cwd();
1605
+ const nonInteractive = isNonInteractive() || args.yes;
1606
+ let template = args.template;
1607
+ let preset = args.preset;
1608
+ let packageManager = args.packageManager;
1609
+ let git = args.git;
1610
+ let name = args.name;
1611
+ logger$3.info("🚀 ubean project initializer");
1612
+ if (!nonInteractive) {
1613
+ name = name || await prompt("Project name", args.dir === "." ? basename(cwd) : args.dir);
1614
+ template = template || await select("Select a template:", TEMPLATES, "starter");
1615
+ preset = preset || await select("Select a preset:", PRESETS, "standard");
1616
+ packageManager = packageManager || await prompt("Package manager (npm/pnpm/yarn/bun)", "npm");
1617
+ if (git === void 0) git = await confirm("Initialize git repository?", true);
1618
+ }
1619
+ const options = {
1620
+ cwd,
1621
+ dir: args.dir,
1622
+ force: args.force,
1623
+ template: template || "starter",
1624
+ preset: preset || "standard",
1625
+ packageManager: packageManager || "npm",
1626
+ git: git !== false,
1627
+ name,
1628
+ nonInteractive
1629
+ };
1630
+ if (!PACKAGE_MANAGERS.includes(options.packageManager)) options.packageManager = "npm";
1631
+ await scaffoldProject(options);
1632
+ }
1633
+ };
1634
+ //#endregion
1635
+ //#region src/prepare.ts
1636
+ const logger$2 = consola.withTag("ubean-cli");
1637
+ async function ensureBuildDir(cwd, buildDir) {
1638
+ const fullPath = join$1(cwd, buildDir);
1639
+ if (!existsSync(fullPath)) mkdirSync(fullPath, { recursive: true });
1640
+ }
1641
+ /**
1642
+ * Detects the package manager used in the project by inspecting lockfiles.
1643
+ * Priority: pnpm > yarn > bun > npm (npm as default fallback).
1644
+ */
1645
+ function detectPackageManager(cwd) {
1646
+ if (existsSync(join$1(cwd, "pnpm-lock.yaml"))) return "pnpm";
1647
+ if (existsSync(join$1(cwd, "yarn.lock"))) return "yarn";
1648
+ if (existsSync(join$1(cwd, "bun.lockb")) || existsSync(join$1(cwd, "bun.lock"))) return "bun";
1649
+ return "npm";
1650
+ }
1651
+ /**
1652
+ * Builds the install command args for a given package manager and package list.
1653
+ * Examples:
1654
+ * pnpm → ['add', '@ubean/ui@^0.1.3']
1655
+ * npm → ['install', '@ubean/ui@^0.1.3']
1656
+ */
1657
+ function buildInstallCommand(pm, packages) {
1658
+ switch (pm) {
1659
+ case "pnpm": return {
1660
+ cmd: "pnpm",
1661
+ args: ["add", ...packages]
1662
+ };
1663
+ case "yarn": return {
1664
+ cmd: "yarn",
1665
+ args: ["add", ...packages]
1666
+ };
1667
+ case "bun": return {
1668
+ cmd: "bun",
1669
+ args: ["add", ...packages]
1670
+ };
1671
+ default: return {
1672
+ cmd: "npm",
1673
+ args: ["install", ...packages]
1674
+ };
1675
+ }
1676
+ }
1677
+ /**
1678
+ * Extracts the bare package name from a module path.
1679
+ * '@ubean/ui/vite' → '@ubean/ui'
1680
+ * '@ubean/electron/vite' → '@ubean/electron'
1681
+ */
1682
+ function extractPackageName(modulePath) {
1683
+ if (modulePath.startsWith("@")) return modulePath.split("/").slice(0, 2).join("/");
1684
+ return modulePath.split("/")[0];
1685
+ }
1686
+ /**
1687
+ * Reads the `ubean` package version from the project's package.json so we can
1688
+ * install matching versions of `@ubean/*` extension packages.
1689
+ * Returns `null` if ubean isn't declared or version can't be parsed.
1690
+ */
1691
+ function getUbeanVersion(cwd) {
1692
+ try {
1693
+ const pkgPath = join$1(cwd, "package.json");
1694
+ if (!existsSync(pkgPath)) return null;
1695
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1696
+ const spec = pkg.dependencies?.ubean || pkg.devDependencies?.ubean;
1697
+ if (!spec) return null;
1698
+ const match = spec.match(/\d+\.\d+\.\d+/);
1699
+ return match ? match[0] : null;
1700
+ } catch {
1701
+ return null;
1702
+ }
1703
+ }
1704
+ /**
1705
+ * Probes whether a module is importable (i.e., installed in node_modules).
1706
+ * Uses dynamic import — same mechanism as `loadBuiltinModule` so detection
1707
+ * behavior matches what the runtime will actually encounter.
1708
+ */
1709
+ async function isModuleInstalled(modulePath) {
1710
+ try {
1711
+ await import(
1712
+ /* @vite-ignore */
1713
+ modulePath
1714
+ );
1715
+ return true;
1716
+ } catch {
1717
+ return false;
1718
+ }
1719
+ }
1720
+ /**
1721
+ * Detects built-in modules enabled in config but not installed, and
1722
+ * auto-installs them using the project's package manager.
1723
+ *
1724
+ * Behavior:
1725
+ * - When `autoInstall` is `false`, only prints warnings with install commands.
1726
+ * - When `autoInstall` is `true` and a package.json exists, runs the install.
1727
+ * - Version pinning: installs `@ubean/<pkg>@^<ubean-version>` to keep in sync.
1728
+ * - Failures fall back to warnings (non-fatal — prepare should still proceed).
1729
+ */
1730
+ async function ensureBuiltinModules(cwd, config, autoInstall) {
1731
+ const missing = [];
1732
+ for (const builtin of BUILTIN_MODULES) {
1733
+ const configValue = config[builtin.key];
1734
+ if (isBuiltinDisabled(configValue)) continue;
1735
+ const packageName = extractPackageName(builtin.modulePath);
1736
+ if (!await isModuleInstalled(builtin.modulePath)) missing.push({
1737
+ key: builtin.key,
1738
+ packageName
1739
+ });
1740
+ }
1741
+ if (missing.length === 0) return;
1742
+ const pm = detectPackageManager(cwd);
1743
+ const ubeanVersion = getUbeanVersion(cwd);
1744
+ const packagesToInstall = missing.map((m) => ubeanVersion ? `${m.packageName}@^${ubeanVersion}` : m.packageName);
1745
+ if (!autoInstall || !existsSync(join$1(cwd, "package.json"))) {
1746
+ for (const m of missing) {
1747
+ const versionSuffix = ubeanVersion ? `@^${ubeanVersion}` : "";
1748
+ logger$2.warn(`Built-in module "${m.key}" is enabled but \`${m.packageName}\` is not installed. Run: ${pm} add ${m.packageName}${versionSuffix}`);
1749
+ }
1750
+ return;
1751
+ }
1752
+ logger$2.info(`Auto-installing missing built-in module packages: ${packagesToInstall.join(", ")}`);
1753
+ logger$2.info(`Using package manager: ${pm}`);
1754
+ const { cmd, args } = buildInstallCommand(pm, packagesToInstall);
1755
+ try {
1756
+ execSync([cmd, ...args].join(" "), {
1757
+ cwd,
1758
+ stdio: "inherit",
1759
+ env: { ...process.env }
1760
+ });
1761
+ logger$2.success(`Installed: ${packagesToInstall.join(", ")}`);
1762
+ } catch (err) {
1763
+ logger$2.error(`Failed to auto-install packages: ${err instanceof Error ? err.message : String(err)}`);
1764
+ for (const m of missing) {
1765
+ const versionSuffix = ubeanVersion ? `@^${ubeanVersion}` : "";
1766
+ logger$2.warn(`Please install manually: ${pm} add ${m.packageName}${versionSuffix}`);
1767
+ }
1768
+ }
1769
+ }
1770
+ const prepareCommand = {
1771
+ meta: {
1772
+ name: "prepare",
1773
+ description: "Generate type definitions and prepare the project (runs automatically before dev/build)"
1774
+ },
1775
+ args: {
1776
+ cwd: {
1777
+ type: "string",
1778
+ description: "Project root directory",
1779
+ default: "."
1780
+ },
1781
+ install: {
1782
+ type: "boolean",
1783
+ description: "Auto-install missing built-in module packages (use --no-install to skip)",
1784
+ default: true
1785
+ }
1786
+ },
1787
+ async run({ args }) {
1788
+ const cwd = resolve$1(args.cwd || process.cwd());
1789
+ logger$2.start(`Preparing ubean project at ${cwd}...`);
1790
+ const config = await loadUbeanConfig(cwd);
1791
+ await ensureBuiltinModules(cwd, config, args.install !== false);
1792
+ const typesDir = ".ubean";
1793
+ await ensureBuildDir(cwd, typesDir);
1794
+ logger$2.info("Scanning project files...");
1795
+ const result = await scanProject({
1796
+ cwd,
1797
+ srcDir: config.srcDir,
1798
+ dirs: config.dir,
1799
+ ignore: config.scanOptions?.ignore
1800
+ });
1801
+ logger$2.info(`Found ${result.apiRoutes.length} API routes, ${result.pages.length} pages, ${result.layouts.length} layouts, ${result.middlewares.length} middlewares`);
1802
+ logger$2.info("Generating type definitions...");
1803
+ const codegenResult = await generateTypes(result, {
1804
+ cwd,
1805
+ srcDir: config.srcDir,
1806
+ buildDir: typesDir,
1807
+ dirs: config.dir,
1808
+ imports: config.imports,
1809
+ components: config.components,
1810
+ composablesDirs: config.imports.dirs,
1811
+ componentsDirs: config.components.dirs
1812
+ });
1813
+ for (const file of codegenResult.generated) logger$2.success(`Generated ${file.replace(`${cwd}/`, "")}`);
1814
+ logger$2.success("Prepare complete!");
1815
+ }
1816
+ };
1817
+ //#endregion
1818
+ //#region src/preview.ts
1819
+ const MIME_TYPES = {
1820
+ ".html": "text/html; charset=utf-8",
1821
+ ".js": "text/javascript; charset=utf-8",
1822
+ ".mjs": "text/javascript; charset=utf-8",
1823
+ ".css": "text/css; charset=utf-8",
1824
+ ".json": "application/json; charset=utf-8",
1825
+ ".svg": "image/svg+xml",
1826
+ ".png": "image/png",
1827
+ ".jpg": "image/jpeg",
1828
+ ".jpeg": "image/jpeg",
1829
+ ".gif": "image/gif",
1830
+ ".webp": "image/webp",
1831
+ ".ico": "image/x-icon",
1832
+ ".woff": "font/woff",
1833
+ ".woff2": "font/woff2",
1834
+ ".ttf": "font/ttf",
1835
+ ".otf": "font/otf",
1836
+ ".wasm": "application/wasm",
1837
+ ".txt": "text/plain; charset=utf-8",
1838
+ ".map": "application/json; charset=utf-8"
1839
+ };
1840
+ /**
1841
+ * 启动静态文件服务器,用于 spa / ssg 模式预览。
1842
+ *
1843
+ * - ssg 模式:优先查找 `<path>/index.html` 或 `<path>.html`,找不到时返回 404
1844
+ * - spa 模式:所有非文件路由回退到 `index.html`(客户端路由接管)
1845
+ *
1846
+ * 使用 Node 内置 `http` + `fs`,避免引入 `sirv` 等依赖。
1847
+ */
1848
+ function startStaticServer(opts) {
1849
+ const { root, port, host, mode } = opts;
1850
+ const spaFallback = mode === "spa";
1851
+ return createServer(async (req, res) => {
1852
+ try {
1853
+ const url = new URL(req.url || "/", `http://${host}`);
1854
+ let pathname = normalize(decodeURIComponent(url.pathname));
1855
+ if (pathname.includes("..")) {
1856
+ res.statusCode = 400;
1857
+ res.end("Bad Request");
1858
+ return;
1859
+ }
1860
+ let filePath = join$1(root, pathname);
1861
+ let fileExists = existsSync(filePath) && (await stat(filePath)).isFile();
1862
+ if (!fileExists && existsSync(filePath) && (await stat(filePath)).isDirectory()) {
1863
+ const indexPath = join$1(filePath, "index.html");
1864
+ if (existsSync(indexPath)) {
1865
+ filePath = indexPath;
1866
+ fileExists = true;
1867
+ }
1868
+ }
1869
+ if (!fileExists && mode === "ssg") {
1870
+ const indexPath = join$1(filePath, "index.html");
1871
+ if (existsSync(indexPath) && (await stat(indexPath)).isFile()) {
1872
+ filePath = indexPath;
1873
+ fileExists = true;
1874
+ }
1875
+ }
1876
+ if (!fileExists && mode === "ssg") {
1877
+ const htmlPath = `${filePath}.html`;
1878
+ if (existsSync(htmlPath) && (await stat(htmlPath)).isFile()) {
1879
+ filePath = htmlPath;
1880
+ fileExists = true;
1881
+ }
1882
+ }
1883
+ if (!fileExists && spaFallback) {
1884
+ const fallbackPath = join$1(root, "index.html");
1885
+ if (existsSync(fallbackPath)) {
1886
+ filePath = fallbackPath;
1887
+ fileExists = true;
1888
+ }
1889
+ }
1890
+ if (!fileExists) {
1891
+ res.statusCode = 404;
1892
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
1893
+ res.end("Not Found");
1894
+ return;
1895
+ }
1896
+ const ext = extname(filePath).toLowerCase();
1897
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
1898
+ const body = await readFile(filePath);
1899
+ res.statusCode = 200;
1900
+ res.setHeader("Content-Type", contentType);
1901
+ res.setHeader("Content-Length", body.length);
1902
+ res.end(body);
1903
+ } catch (err) {
1904
+ res.statusCode = 500;
1905
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
1906
+ res.end(err instanceof Error ? err.message : String(err));
1907
+ }
1908
+ }).listen(port, host);
1909
+ }
1910
+ const logger$1 = consola.withTag("ubean-cli");
1911
+ const previewCommand = {
1912
+ meta: {
1913
+ name: "preview",
1914
+ description: "Preview the production build locally"
1915
+ },
1916
+ args: {
1917
+ port: {
1918
+ type: "string",
1919
+ description: "Port to listen on"
1920
+ },
1921
+ host: {
1922
+ type: "string",
1923
+ description: "Host to listen on"
1924
+ },
1925
+ strictPort: {
1926
+ type: "boolean",
1927
+ description: "Exit if the port is already in use, instead of auto-incrementing",
1928
+ default: false
1929
+ },
1930
+ cwd: {
1931
+ type: "string",
1932
+ description: "Project root directory",
1933
+ default: "."
1934
+ }
1935
+ },
1936
+ async run({ args }) {
1937
+ const cwd = resolve$1(args.cwd || process.cwd());
1938
+ logger$1.start("Starting ubean preview server...");
1939
+ registerBuiltinPresets();
1940
+ const config = await loadUbeanConfig(cwd);
1941
+ const preset = resolvePresetByName(config.build.preset);
1942
+ logger$1.info(`Preset: ${preset.name}`);
1943
+ const outputDir = config.build.outputDir || "dist";
1944
+ const mode = config.mode;
1945
+ const host = args.host || config.preview.host;
1946
+ const strictPort = args.strictPort ?? config.preview.strictPort;
1947
+ const requestedPort = Number(args.port) || config.preview.port;
1948
+ let actualPort;
1949
+ try {
1950
+ actualPort = await findAvailablePort(requestedPort, {
1951
+ host,
1952
+ strictPort
1953
+ });
1954
+ } catch (err) {
1955
+ if (err?.code === "EADDRINUSE") logger$1.error(`Port ${requestedPort} is already in use${host ? ` on ${host}` : ""}. Try a different port or remove the --strictPort flag.`);
1956
+ else logger$1.error(`Failed to resolve a port: ${err?.message || String(err)}`);
1957
+ process.exit(1);
1958
+ }
1959
+ if (actualPort !== requestedPort) logger$1.warn(`Port ${requestedPort} is in use, trying ${actualPort} instead.`);
1960
+ const label = (text) => dim(text);
1961
+ const printBanner = (port, modeLabel) => {
1962
+ const bannerUrl = `http://${host}:${port}`;
1963
+ logger$1.box(`${green(bold("🚀 ubean preview server ready"))}\n\n → ${label("Local:")} ${cyan(bannerUrl)}\n → ${label("Mode:")} ${dim(modeLabel)}\n → ${label("Preset:")} ${cyan(preset.name)}\n → ${dim("Press Ctrl+C to stop")}`);
1964
+ };
1965
+ if (mode === "spa" || mode === "ssg") {
1966
+ const staticRoot = join$1(cwd, outputDir, "public");
1967
+ if (!existsSync(staticRoot)) {
1968
+ logger$1.error(`Build output not found: ${staticRoot}`);
1969
+ logger$1.info("Run `ubean build` first to create a production build.");
1970
+ process.exit(1);
1971
+ }
1972
+ const server = startStaticServer({
1973
+ root: staticRoot,
1974
+ port: actualPort,
1975
+ host,
1976
+ mode
1977
+ });
1978
+ printBanner(actualPort, `static (${mode})`);
1979
+ const cleanup = () => {
1980
+ server.close(() => process.exit(0));
1981
+ setTimeout(() => process.exit(0), 1e3).unref();
1982
+ };
1983
+ process.on("SIGINT", cleanup);
1984
+ process.on("SIGTERM", cleanup);
1985
+ server.on("error", (err) => {
1986
+ logger$1.error(`Preview server error: ${err.message || err}`);
1987
+ process.exit(1);
1988
+ });
1989
+ return;
1990
+ }
1991
+ const presetName = preset.name;
1992
+ let serverFile;
1993
+ if (presetName === "node" || presetName === "bun" || presetName === "deno") serverFile = "server.mjs";
1994
+ else if (presetName === "cloudflare") {
1995
+ logger$1.error("Cloudflare preset preview is not supported yet. Use `wrangler dev` instead.");
1996
+ process.exit(1);
1997
+ } else {
1998
+ serverFile = "handler.mjs";
1999
+ logger$1.warn(`Preset "${presetName}" uses a fetch handler entry. Preview may not work as a standalone server.`);
2000
+ }
2001
+ const serverPath = join$1(cwd, outputDir, "server", serverFile);
2002
+ if (!existsSync(serverPath)) {
2003
+ logger$1.error(`Build output not found: ${serverPath}`);
2004
+ logger$1.info("Run `ubean build` first to create a production build.");
2005
+ process.exit(1);
2006
+ }
2007
+ const { child, exited } = spawnPreviewServer({
2008
+ serverPath,
2009
+ cwd,
2010
+ port: actualPort,
2011
+ host,
2012
+ strictPort
2013
+ });
2014
+ try {
2015
+ await waitForPort(actualPort, {
2016
+ host,
2017
+ retries: 40,
2018
+ delay: 250
2019
+ });
2020
+ } catch {
2021
+ if (exited.value) logger$1.error(`Preview server exited before becoming ready on port ${actualPort}.`);
2022
+ else logger$1.warn(`Preview server on port ${actualPort} is not responding yet, the banner may be premature.`);
2023
+ }
2024
+ printBanner(actualPort, "production");
2025
+ const cleanup = () => {
2026
+ child.kill("SIGTERM");
2027
+ process.exit(0);
2028
+ };
2029
+ process.on("SIGINT", cleanup);
2030
+ process.on("SIGTERM", cleanup);
2031
+ child.on("exit", (code) => {
2032
+ if (code !== 0 && code !== null) logger$1.error(`Preview server exited with code ${code}`);
2033
+ process.exit(code ?? 0);
2034
+ });
2035
+ }
2036
+ };
2037
+ function spawnPreviewServer(opts) {
2038
+ const exited = { value: false };
2039
+ const child = spawn("node", [opts.serverPath], {
2040
+ cwd: opts.cwd,
2041
+ env: {
2042
+ ...process.env,
2043
+ PORT: String(opts.port),
2044
+ HOST: opts.host
2045
+ },
2046
+ stdio: [
2047
+ "inherit",
2048
+ "inherit",
2049
+ "pipe"
2050
+ ]
2051
+ });
2052
+ let addrInUseReported = false;
2053
+ child.stderr?.on("data", (chunk) => {
2054
+ const text = chunk.toString();
2055
+ process.stderr.write(text);
2056
+ if (!addrInUseReported && /EADDRINUSE/.test(text)) {
2057
+ addrInUseReported = true;
2058
+ logger$1.error(`Port ${opts.port} is already in use${opts.host ? ` on ${opts.host}` : ""}. Try a different port${opts.strictPort ? " or remove the --strictPort flag" : ""}.`);
2059
+ }
2060
+ });
2061
+ child.on("exit", () => {
2062
+ exited.value = true;
2063
+ });
2064
+ return {
2065
+ child,
2066
+ exited
2067
+ };
2068
+ }
2069
+ //#endregion
2070
+ //#region src/scaffold-commands.ts
2071
+ const logger = consola.withTag("ubean-cli");
2072
+ const COMMAND_META = {
2073
+ api: {
2074
+ name: "api",
2075
+ description: "Scaffold API routes",
2076
+ pathLabel: "API route path",
2077
+ pathExample: "users/[id]"
2078
+ },
2079
+ layout: {
2080
+ name: "layout",
2081
+ description: "Scaffold layout components",
2082
+ pathLabel: "Layout path",
2083
+ pathExample: "admin"
2084
+ },
2085
+ middleware: {
2086
+ name: "middleware",
2087
+ description: "Scaffold middleware",
2088
+ pathLabel: "Middleware path",
2089
+ pathExample: "auth"
2090
+ },
2091
+ cron: {
2092
+ name: "cron",
2093
+ description: "Scaffold scheduled cron tasks",
2094
+ pathLabel: "Cron task path",
2095
+ pathExample: "daily-cleanup"
2096
+ },
2097
+ plugin: {
2098
+ name: "plugin",
2099
+ description: "Scaffold ubean plugins",
2100
+ pathLabel: "Plugin path",
2101
+ pathExample: "my-plugin"
2102
+ }
2103
+ };
2104
+ function createScaffoldCommand(type) {
2105
+ const meta = COMMAND_META[type];
2106
+ return {
2107
+ meta: {
2108
+ name: meta.name,
2109
+ description: meta.description
2110
+ },
2111
+ subCommands: {
2112
+ add: {
2113
+ meta: {
2114
+ name: "add",
2115
+ description: `Add a new ${type}`
2116
+ },
2117
+ args: {
2118
+ path: {
2119
+ type: "positional",
2120
+ description: `${meta.pathLabel} (e.g., ${meta.pathExample})`
2121
+ },
2122
+ ...type === "api" ? { method: {
2123
+ type: "string",
2124
+ description: "HTTP method for API routes (GET, POST, PUT, PATCH, DELETE)",
2125
+ default: "GET"
2126
+ } } : {},
2127
+ ...type === "cron" ? { schedule: {
2128
+ type: "string",
2129
+ description: "Cron schedule expression (e.g., \"0 0 * * *\" for daily at midnight)",
2130
+ default: "* * * * *"
2131
+ } } : {},
2132
+ force: {
2133
+ type: "boolean",
2134
+ description: "Overwrite existing files (creates .bak backup)",
2135
+ default: false,
2136
+ alias: "f"
2137
+ },
2138
+ dry: {
2139
+ type: "boolean",
2140
+ description: "Show what would be created without writing files",
2141
+ default: false
2142
+ }
2143
+ },
2144
+ async run({ args }) {
2145
+ const result = await scaffold({
2146
+ cwd: process.cwd(),
2147
+ type,
2148
+ path: args.path,
2149
+ method: args.method,
2150
+ schedule: args.schedule,
2151
+ force: args.force,
2152
+ dry: args.dry
2153
+ });
2154
+ for (const file of result.created) logger.success(`${args.dry ? "[dry-run] Would create" : "Created"} ${file}`);
2155
+ for (const file of result.skipped) logger.warn(`Skipped ${file} (already exists, use --force to overwrite)`);
2156
+ for (const err of result.errors) logger.error(err);
2157
+ }
2158
+ },
2159
+ delete: {
2160
+ meta: {
2161
+ name: "delete",
2162
+ description: `Delete a ${type} (creates backup by default)`
2163
+ },
2164
+ args: {
2165
+ path: {
2166
+ type: "positional",
2167
+ description: `${meta.pathLabel} to delete`
2168
+ },
2169
+ force: {
2170
+ type: "boolean",
2171
+ description: "Delete permanently without creating backup",
2172
+ default: false,
2173
+ alias: "f"
2174
+ },
2175
+ dry: {
2176
+ type: "boolean",
2177
+ description: "Show what would be deleted",
2178
+ default: false
2179
+ }
2180
+ },
2181
+ async run({ args }) {
2182
+ const result = await deleteScaffold({
2183
+ cwd: process.cwd(),
2184
+ type,
2185
+ path: args.path,
2186
+ force: args.force,
2187
+ dry: args.dry
2188
+ });
2189
+ for (const file of result.deleted) logger.success(`${args.dry ? "[dry-run] Would delete" : args.force ? "Deleted" : "Deleted (backup created)"} ${file}`);
2190
+ for (const err of result.errors) logger.error(err);
2191
+ }
2192
+ },
2193
+ recovery: {
2194
+ meta: {
2195
+ name: "recovery",
2196
+ description: `Recover a deleted ${type} from .bak backup`
2197
+ },
2198
+ args: {
2199
+ path: {
2200
+ type: "positional",
2201
+ description: `${meta.pathLabel} to recover`
2202
+ },
2203
+ dry: {
2204
+ type: "boolean",
2205
+ description: "Check if recovery is possible",
2206
+ default: false
2207
+ }
2208
+ },
2209
+ async run({ args }) {
2210
+ const result = await recoverScaffold({
2211
+ cwd: process.cwd(),
2212
+ type,
2213
+ path: args.path,
2214
+ dry: args.dry
2215
+ });
2216
+ for (const file of result.restored) logger.success(`${args.dry ? "[dry-run] Backup exists for" : "Recovered"} ${file}`);
2217
+ for (const err of result.errors) logger.error(err);
2218
+ }
2219
+ },
2220
+ list: {
2221
+ meta: {
2222
+ name: "list",
2223
+ description: `List existing ${type} files`
2224
+ },
2225
+ async run() {
2226
+ const files = await listScaffoldableFiles(process.cwd(), type);
2227
+ if (files.length === 0) {
2228
+ logger.info(`No ${type} files found`);
2229
+ return;
2230
+ }
2231
+ logger.info(`Found ${files.length} ${type}(s):`);
2232
+ for (const file of files) logger.log(` ${file}`);
2233
+ }
2234
+ }
2235
+ }
2236
+ };
2237
+ }
2238
+ //#endregion
2239
+ //#region src/cli.ts
2240
+ /**
2241
+ * @ubean/cli — CLI 入口(运行时调用 `runMain`)
2242
+ *
2243
+ * 此文件仅由 `bin/ubean-next.mjs` 导入,不应被库代码 import
2244
+ * (会产生启动 CLI 的副作用)。
2245
+ *
2246
+ * 库代码请从 `@ubean/cli`(主入口)导入 scaffold / fs-ops / templates。
2247
+ */
2248
+ runMain(defineCommand({
2249
+ meta: {
2250
+ name: "ubean",
2251
+ version: "0.0.1",
2252
+ description: "Vue meta framework built on Vite-Plus with Hono"
2253
+ },
2254
+ subCommands: {
2255
+ dev: devCommand,
2256
+ build: buildCommand,
2257
+ prepare: prepareCommand,
2258
+ preview: previewCommand,
2259
+ init: initCommand,
2260
+ page: pageCommand,
2261
+ env: envCommand,
2262
+ config: configCommand,
2263
+ devtools: devtoolsCommand,
2264
+ api: createScaffoldCommand("api"),
2265
+ layout: createScaffoldCommand("layout"),
2266
+ middleware: createScaffoldCommand("middleware"),
2267
+ cron: createScaffoldCommand("cron"),
2268
+ plugin: createScaffoldCommand("plugin")
2269
+ }
2270
+ }));
2271
+ //#endregion
2272
+ export {};