@ubean/cli 0.1.1 → 0.1.3

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