@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.
@@ -0,0 +1,672 @@
1
+ import { existsSync, promises } from "node:fs";
2
+ import { dirname, extname, join, relative, resolve } from "node:path";
3
+ import { consola } from "consola";
4
+ //#region src/shared/fs-ops.ts
5
+ function resolvePath(cwd, ...paths) {
6
+ return resolve(cwd, ...paths);
7
+ }
8
+ async function ensureDir(dir) {
9
+ await promises.mkdir(dir, { recursive: true });
10
+ }
11
+ async function exists(path) {
12
+ try {
13
+ await promises.access(path);
14
+ return true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+ function existsSync_(path) {
20
+ return existsSync(path);
21
+ }
22
+ async function readFile(path, encoding = "utf-8") {
23
+ return promises.readFile(path, encoding);
24
+ }
25
+ async function writeFile(path, content, encoding = "utf-8") {
26
+ await ensureDir(dirname(path));
27
+ await promises.writeFile(path, content, encoding);
28
+ }
29
+ async function appendFile(path, content, encoding = "utf-8") {
30
+ await promises.appendFile(path, content, encoding);
31
+ }
32
+ async function remove(path) {
33
+ await promises.rm(path, {
34
+ recursive: true,
35
+ force: true
36
+ });
37
+ }
38
+ async function copyFile(src, dest) {
39
+ await ensureDir(dirname(dest));
40
+ await promises.copyFile(src, dest);
41
+ }
42
+ async function copyDir(src, dest, options) {
43
+ await ensureDir(dest);
44
+ const entries = await promises.readdir(src, { withFileTypes: true });
45
+ for (const entry of entries) {
46
+ const srcPath = join(src, entry.name);
47
+ const destPath = join(dest, entry.name);
48
+ if (options?.filter && !options.filter(srcPath)) continue;
49
+ if (entry.isDirectory()) await copyDir(srcPath, destPath, options);
50
+ else await copyFile(srcPath, destPath);
51
+ }
52
+ }
53
+ async function readDir(path) {
54
+ return promises.readdir(path);
55
+ }
56
+ async function readJson(path) {
57
+ const content = await readFile(path);
58
+ return JSON.parse(content);
59
+ }
60
+ async function writeJson(path, data, indent = 2) {
61
+ await writeFile(path, `${JSON.stringify(data, null, indent)}\n`);
62
+ }
63
+ async function createBackup(path, options = {}) {
64
+ const backupPath = `${path}${options.backupSuffix || ".bak"}`;
65
+ if (!await exists(path)) return null;
66
+ await copyFile(path, backupPath);
67
+ if (options.removeOriginal) await remove(path);
68
+ return backupPath;
69
+ }
70
+ async function restoreBackup(path, options = {}) {
71
+ const backupPath = `${path}${options.backupSuffix || ".bak"}`;
72
+ if (!await exists(backupPath)) return false;
73
+ await copyFile(backupPath, path);
74
+ return true;
75
+ }
76
+ async function removeBackup(path, options = {}) {
77
+ await remove(`${path}${options.backupSuffix || ".bak"}`);
78
+ }
79
+ async function listFiles(dir, pattern) {
80
+ const results = [];
81
+ async function walk(current) {
82
+ const entries = await promises.readdir(current, { withFileTypes: true });
83
+ for (const entry of entries) {
84
+ const fullPath = join(current, entry.name);
85
+ if (entry.isDirectory()) {
86
+ if (entry.name === "node_modules" || entry.name.startsWith(".") && entry.name !== ".ubean") continue;
87
+ await walk(fullPath);
88
+ } else if (!pattern || pattern.test(entry.name)) results.push(fullPath);
89
+ }
90
+ }
91
+ if (await exists(dir)) await walk(dir);
92
+ return results;
93
+ }
94
+ function createFsOps(cwd) {
95
+ return {
96
+ cwd,
97
+ resolve: (...paths) => resolvePath(cwd, ...paths),
98
+ exists: (path) => exists(resolvePath(cwd, path)),
99
+ existsSync: (path) => existsSync_(resolvePath(cwd, path)),
100
+ readFile: (path, enc) => readFile(resolvePath(cwd, path), enc),
101
+ writeFile: (path, content, enc) => writeFile(resolvePath(cwd, path), content, enc),
102
+ appendFile: (path, content, enc) => appendFile(resolvePath(cwd, path), content, enc),
103
+ remove: (path) => remove(resolvePath(cwd, path)),
104
+ ensureDir: (path) => ensureDir(resolvePath(cwd, path)),
105
+ copyFile: (src, dest) => copyFile(resolvePath(cwd, src), resolvePath(cwd, dest)),
106
+ copyDir: (src, dest, opts) => copyDir(resolvePath(cwd, src), resolvePath(cwd, dest), opts),
107
+ readDir: (path) => readDir(resolvePath(cwd, path)),
108
+ readJson: (path) => readJson(resolvePath(cwd, path)),
109
+ writeJson: (path, data, indent) => writeJson(resolvePath(cwd, path), data, indent),
110
+ createBackup: (path, opts) => createBackup(resolvePath(cwd, path), opts),
111
+ restoreBackup: (path, opts) => restoreBackup(resolvePath(cwd, path), opts),
112
+ removeBackup: (path, opts) => removeBackup(resolvePath(cwd, path), opts),
113
+ listFiles: (dir, pattern) => listFiles(resolvePath(cwd, dir || "."), pattern),
114
+ relative: (to) => relative(cwd, resolve(cwd, to))
115
+ };
116
+ }
117
+ //#endregion
118
+ //#region src/shared/templates.ts
119
+ const DEFAULT_DELIMITERS = ["{{", "}}"];
120
+ function renderTemplate(template, options = {}) {
121
+ const vars = options.variables || {};
122
+ const [open, close] = options.delimiters || DEFAULT_DELIMITERS;
123
+ const openEsc = open.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
124
+ const closeEsc = close.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
125
+ const pattern = new RegExp(`${openEsc}\\s*([\\w.]+)\\s*${closeEsc}`, "g");
126
+ return template.replace(pattern, (_match, key) => {
127
+ const value = getNestedValue(vars, key);
128
+ if (value === void 0 || value === null) return `${open}${key}${close}`;
129
+ return String(value);
130
+ });
131
+ }
132
+ function getNestedValue(obj, path) {
133
+ const parts = path.split(".");
134
+ let current = obj;
135
+ for (const part of parts) {
136
+ if (current == null || typeof current !== "object") return void 0;
137
+ current = current[part];
138
+ }
139
+ return current;
140
+ }
141
+ function toKebabCase(str) {
142
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
143
+ }
144
+ function toPascalCase(str) {
145
+ return str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^(.)/, (c) => c.toUpperCase());
146
+ }
147
+ function toCamelCase(str) {
148
+ const pascal = toPascalCase(str);
149
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
150
+ }
151
+ const PAGE_TEMPLATE = `<script setup lang="ts">
152
+ definePage({
153
+ meta: {
154
+ title: '{{name}}'
155
+ }
156
+ });
157
+ <\/script>
158
+
159
+ <template>
160
+ <div class="{{kebabName}}-page">
161
+ <div>{{name}}</div>
162
+ </div>
163
+ </template>
164
+
165
+ `;
166
+ const API_TEMPLATE = `import { defineHandler } from 'ubean';
167
+
168
+ export default defineHandler(async c => {
169
+ return c.json({ message: '{{name}} endpoint' });
170
+ });
171
+ `;
172
+ const MIDDLEWARE_TEMPLATE = `import { defineMiddleware } from 'ubean';
173
+
174
+ export default defineMiddleware(async (c, next) => {
175
+ console.log('{{name}} middleware');
176
+ await next();
177
+ });
178
+ `;
179
+ const LAYOUT_TEMPLATE = `<script setup lang="ts">
180
+ definePage({
181
+ meta: {
182
+ title: '{{name}}'
183
+ }
184
+ });
185
+ <\/script>
186
+
187
+ <template>
188
+ <div class="{{kebabName}}">
189
+ <slot />
190
+ </div>
191
+ </template>
192
+ `;
193
+ const CRON_TEMPLATE = `import { defineScheduled } from 'ubean';
194
+
195
+ export default defineScheduled({
196
+ name: '{{name}}',
197
+ schedule: '{{schedule}}'
198
+ }, async () => {
199
+ console.log('Running {{name}} cron task');
200
+ });
201
+ `;
202
+ const PLUGIN_TEMPLATE = `import { definePlugin } from 'ubean';
203
+
204
+ export default definePlugin({
205
+ name: '{{kebabName}}',
206
+ setup() {
207
+ console.log('{{name}} plugin setup');
208
+ }
209
+ });
210
+ `;
211
+ function renderPageTemplate(data) {
212
+ return renderTemplate(PAGE_TEMPLATE, { variables: {
213
+ ...data,
214
+ kebabName: toKebabCase(data.name)
215
+ } });
216
+ }
217
+ function renderApiTemplate(data) {
218
+ return renderTemplate(API_TEMPLATE, { variables: {
219
+ ...data,
220
+ kebabName: toKebabCase(data.name)
221
+ } });
222
+ }
223
+ function renderMiddlewareTemplate(data) {
224
+ return renderTemplate(MIDDLEWARE_TEMPLATE, { variables: data });
225
+ }
226
+ function renderLayoutTemplate(data) {
227
+ const kebabName = toKebabCase(data.name);
228
+ return renderTemplate(LAYOUT_TEMPLATE, { variables: {
229
+ ...data,
230
+ kebabName,
231
+ pascalName: toPascalCase(data.name)
232
+ } });
233
+ }
234
+ function renderCronTemplate(data) {
235
+ return renderTemplate(CRON_TEMPLATE, { variables: {
236
+ ...data,
237
+ kebabName: toKebabCase(data.name)
238
+ } });
239
+ }
240
+ function renderPluginTemplate(data) {
241
+ const kebabName = toKebabCase(data.name);
242
+ return renderTemplate(PLUGIN_TEMPLATE, { variables: {
243
+ ...data,
244
+ kebabName,
245
+ pascalName: toPascalCase(data.name)
246
+ } });
247
+ }
248
+ //#endregion
249
+ //#region src/page.ts
250
+ const logger = consola.withTag("ubean-cli");
251
+ function getSrcDir(cwd) {
252
+ return join(cwd, "src");
253
+ }
254
+ function getDirForType(cwd, type) {
255
+ const srcDir = getSrcDir(cwd);
256
+ switch (type) {
257
+ case "page":
258
+ case "reuse": return join(srcDir, "pages");
259
+ case "api": return join(srcDir, "routes");
260
+ case "layout": return join(srcDir, "layouts");
261
+ case "middleware": return join(srcDir, "middleware");
262
+ case "cron": return join(srcDir, "server", "crons");
263
+ case "plugin": return join(srcDir, "plugins");
264
+ default: return srcDir;
265
+ }
266
+ }
267
+ /** Resolve a baseDir that may be relative or absolute into an absolute path. */
268
+ function resolveBaseDir(cwd, baseDir) {
269
+ return baseDir.startsWith("/") ? baseDir : join(cwd, baseDir);
270
+ }
271
+ function resolveTargetPath(baseDir, path, type) {
272
+ let normalizedPath = path.startsWith("/") ? path.slice(1) : path;
273
+ if (normalizedPath === "" || normalizedPath === "/") normalizedPath = type === "page" || type === "reuse" ? "index" : "index";
274
+ if (type === "page" || type === "layout" || type === "reuse") {
275
+ if (type === "page" || type === "layout") {
276
+ if (normalizedPath.endsWith(".vue")) return join(baseDir, normalizedPath);
277
+ const ext = ".vue";
278
+ if (normalizedPath.endsWith("/") || normalizedPath === "") return join(baseDir, normalizedPath, `index${ext}`);
279
+ return join(baseDir, `${normalizedPath}${ext}`);
280
+ }
281
+ if (normalizedPath.endsWith(".reuse.ts")) return join(baseDir, normalizedPath);
282
+ if (normalizedPath.endsWith(".ts")) return join(baseDir, normalizedPath.replace(/\.ts$/, ".reuse.ts"));
283
+ const ext = ".reuse.ts";
284
+ if (normalizedPath.endsWith("/") || normalizedPath === "") return join(baseDir, normalizedPath, `index${ext}`);
285
+ return join(baseDir, `${normalizedPath}${ext}`);
286
+ }
287
+ if (normalizedPath.endsWith(".ts") || normalizedPath.endsWith(".js")) return join(baseDir, normalizedPath);
288
+ const ext = ".ts";
289
+ if (normalizedPath.endsWith("/") || normalizedPath === "") return join(baseDir, normalizedPath, `index${ext}`);
290
+ if ((type === "api" || type === "middleware" || type === "cron" || type === "plugin") && !normalizedPath.includes(".")) return join(baseDir, `${normalizedPath}${ext}`);
291
+ return join(baseDir, normalizedPath);
292
+ }
293
+ function sanitizeNameSegment(seg) {
294
+ return seg.replace(/^\[\.\.\.(.+)\]$/, "All$1").replace(/^\[\[(.+)\]\]$/, "$1Optional").replace(/^\[(.+)\]$/, "$1").replace(/[^a-zA-Z0-9$_]/g, "");
295
+ }
296
+ function extractNameFromPath(filePath, _type) {
297
+ const parts = filePath.split("/");
298
+ const basename = parts.pop() || "";
299
+ let nameWithoutExt = basename.replace(extname(basename), "");
300
+ if (nameWithoutExt.endsWith(".reuse")) nameWithoutExt = nameWithoutExt.slice(0, -6);
301
+ const segments = [];
302
+ for (const part of parts) {
303
+ const cleaned = sanitizeNameSegment(part);
304
+ if (cleaned) segments.push(toPascalCase(cleaned));
305
+ }
306
+ if (nameWithoutExt !== "index") segments.push(toPascalCase(sanitizeNameSegment(nameWithoutExt)));
307
+ else if (segments.length === 0) return "Index";
308
+ return segments.join("") || "Index";
309
+ }
310
+ function getTemplateContent(type, name, path, method, schedule) {
311
+ switch (type) {
312
+ case "page": return renderPageTemplate({
313
+ name,
314
+ path: `/${path.replace(/\.vue$/, "").replace(/\/index$/, "")}`,
315
+ kebabName: toKebabCase(name),
316
+ pascalName: name,
317
+ camelName: toCamelCase(name)
318
+ });
319
+ case "reuse": return `import { definePage } from 'ubean';
320
+
321
+ export default definePage({
322
+ name: '${name}'
323
+ });
324
+ `;
325
+ case "api": return renderApiTemplate({
326
+ name,
327
+ method: method || "GET",
328
+ path: `/api/${path.replace(/\.ts$/, "").replace(/\/index$/, "")}`,
329
+ kebabName: toKebabCase(name)
330
+ });
331
+ case "layout": return renderLayoutTemplate({
332
+ name,
333
+ path: `/${path.replace(/\.vue$/, "").replace(/\/index$/, "")}`,
334
+ pascalName: toPascalCase(name)
335
+ });
336
+ case "middleware": return `import { defineMiddleware } from 'ubean';
337
+
338
+ export default defineMiddleware(async (c, next) => {
339
+ console.log('${name} middleware');
340
+ await next();
341
+ });
342
+ `;
343
+ case "cron": return renderCronTemplate({
344
+ name,
345
+ schedule: schedule || "* * * * *",
346
+ kebabName: toKebabCase(name)
347
+ });
348
+ case "plugin": return renderPluginTemplate({
349
+ name,
350
+ kebabName: toKebabCase(name),
351
+ pascalName: toPascalCase(name)
352
+ });
353
+ default: return "";
354
+ }
355
+ }
356
+ async function scaffold(options) {
357
+ const cwd = options.cwd || process.cwd();
358
+ const fs = createFsOps(cwd);
359
+ const result = {
360
+ created: [],
361
+ deleted: [],
362
+ restored: [],
363
+ skipped: [],
364
+ errors: []
365
+ };
366
+ try {
367
+ const relativePath = resolveTargetPath(options.baseDir ? resolveBaseDir(cwd, options.baseDir) : getDirForType(cwd, options.type), options.path, options.type).replace(`${cwd}/`, "");
368
+ if (await fs.exists(relativePath)) {
369
+ if (!options.force) {
370
+ result.skipped.push(relativePath);
371
+ return result;
372
+ }
373
+ await fs.createBackup(relativePath);
374
+ }
375
+ if (options.dry) {
376
+ result.created.push(relativePath);
377
+ return result;
378
+ }
379
+ const name = extractNameFromPath(relativePath, options.type);
380
+ const content = getTemplateContent(options.type, name, options.path, options.method, options.schedule);
381
+ await fs.writeFile(relativePath, content);
382
+ result.created.push(relativePath);
383
+ } catch (err) {
384
+ result.errors.push(err instanceof Error ? err.message : String(err));
385
+ }
386
+ return result;
387
+ }
388
+ async function deleteScaffold(options) {
389
+ const cwd = options.cwd || process.cwd();
390
+ const fs = createFsOps(cwd);
391
+ const result = {
392
+ created: [],
393
+ deleted: [],
394
+ restored: [],
395
+ skipped: [],
396
+ errors: []
397
+ };
398
+ try {
399
+ const relativePath = resolveTargetPath(options.baseDir ? resolveBaseDir(cwd, options.baseDir) : getDirForType(cwd, options.type), options.path, options.type).replace(`${cwd}/`, "");
400
+ if (!await fs.exists(relativePath)) {
401
+ result.errors.push(`${relativePath} does not exist`);
402
+ return result;
403
+ }
404
+ if (options.dry) {
405
+ result.deleted.push(relativePath);
406
+ return result;
407
+ }
408
+ if (options.force) {
409
+ await fs.remove(relativePath);
410
+ result.deleted.push(relativePath);
411
+ return result;
412
+ }
413
+ await fs.createBackup(relativePath, { removeOriginal: true });
414
+ result.deleted.push(relativePath);
415
+ } catch (err) {
416
+ result.errors.push(err instanceof Error ? err.message : String(err));
417
+ }
418
+ return result;
419
+ }
420
+ async function recoverScaffold(options) {
421
+ const cwd = options.cwd || process.cwd();
422
+ const fs = createFsOps(cwd);
423
+ const result = {
424
+ created: [],
425
+ deleted: [],
426
+ restored: [],
427
+ skipped: [],
428
+ errors: []
429
+ };
430
+ try {
431
+ const relativePath = resolveTargetPath(options.baseDir ? resolveBaseDir(cwd, options.baseDir) : getDirForType(cwd, options.type), options.path, options.type).replace(`${cwd}/`, "");
432
+ if (options.dry) {
433
+ const backupPath = `${relativePath}.bak`;
434
+ if (await fs.exists(backupPath)) result.restored.push(relativePath);
435
+ else result.errors.push(`No backup found for ${relativePath}`);
436
+ return result;
437
+ }
438
+ if (await fs.restoreBackup(relativePath)) {
439
+ result.restored.push(relativePath);
440
+ await fs.removeBackup(relativePath);
441
+ } else result.errors.push(`No backup found for ${relativePath}`);
442
+ } catch (err) {
443
+ result.errors.push(err instanceof Error ? err.message : String(err));
444
+ }
445
+ return result;
446
+ }
447
+ async function listScaffoldableFiles(cwd, type, baseDir) {
448
+ const fs = createFsOps(cwd);
449
+ const relativeBase = (baseDir ? resolveBaseDir(cwd, baseDir) : getDirForType(cwd, type)).replace(`${cwd}/`, "");
450
+ return (await fs.listFiles(relativeBase)).map((f) => f.replace(`${cwd}/`, ""));
451
+ }
452
+ const scaffoldTypes = [
453
+ "page",
454
+ "api",
455
+ "layout",
456
+ "middleware",
457
+ "cron",
458
+ "plugin"
459
+ ];
460
+ const allTypes = [
461
+ "page",
462
+ "api",
463
+ "layout",
464
+ "middleware",
465
+ "reuse",
466
+ "cron",
467
+ "plugin"
468
+ ];
469
+ const pageCommand = {
470
+ meta: {
471
+ name: "page",
472
+ description: "Scaffold pages, api routes, layouts, middleware, crons, and plugins"
473
+ },
474
+ subCommands: {
475
+ add: {
476
+ meta: {
477
+ name: "add",
478
+ description: "Add a new page, api route, layout, middleware, cron, or plugin"
479
+ },
480
+ args: {
481
+ path: {
482
+ type: "positional",
483
+ description: "Route path (e.g., users/[id], api/users, crons/daily)"
484
+ },
485
+ type: {
486
+ type: "string",
487
+ description: "Type: page, api, layout, middleware, cron, plugin",
488
+ default: "page"
489
+ },
490
+ method: {
491
+ type: "string",
492
+ description: "HTTP method for API routes (GET, POST, etc.)",
493
+ default: "GET"
494
+ },
495
+ schedule: {
496
+ type: "string",
497
+ description: "Cron schedule expression for cron tasks",
498
+ default: "* * * * *"
499
+ },
500
+ force: {
501
+ type: "boolean",
502
+ description: "Overwrite existing files (creates .bak backup)",
503
+ default: false,
504
+ alias: "f"
505
+ },
506
+ dry: {
507
+ type: "boolean",
508
+ description: "Show what would be created without writing files",
509
+ default: false
510
+ }
511
+ },
512
+ async run({ args }) {
513
+ const type = args.type;
514
+ if (!scaffoldTypes.includes(type)) {
515
+ logger.error(`Invalid type: ${type}. Must be one of: ${scaffoldTypes.join(", ")}`);
516
+ return;
517
+ }
518
+ const result = await scaffold({
519
+ cwd: process.cwd(),
520
+ type,
521
+ path: args.path,
522
+ method: args.method,
523
+ schedule: args.schedule,
524
+ force: args.force,
525
+ dry: args.dry
526
+ });
527
+ for (const file of result.created) logger.success(`${args.dry ? "[dry-run] Would create" : "Created"} ${file}`);
528
+ for (const file of result.skipped) logger.warn(`Skipped ${file} (already exists, use --force to overwrite)`);
529
+ for (const err of result.errors) logger.error(err);
530
+ }
531
+ },
532
+ "add-reuse": {
533
+ meta: {
534
+ name: "add-reuse",
535
+ description: "Add a new reusable page component (.reuse.ts)"
536
+ },
537
+ args: {
538
+ path: {
539
+ type: "positional",
540
+ description: "Reusable page path (e.g., users/[id])"
541
+ },
542
+ force: {
543
+ type: "boolean",
544
+ description: "Overwrite existing files (creates .bak backup)",
545
+ default: false,
546
+ alias: "f"
547
+ },
548
+ dry: {
549
+ type: "boolean",
550
+ description: "Show what would be created without writing files",
551
+ default: false
552
+ }
553
+ },
554
+ async run({ args }) {
555
+ const result = await scaffold({
556
+ cwd: process.cwd(),
557
+ type: "reuse",
558
+ path: args.path,
559
+ force: args.force,
560
+ dry: args.dry
561
+ });
562
+ for (const file of result.created) logger.success(`${args.dry ? "[dry-run] Would create" : "Created"} ${file}`);
563
+ for (const file of result.skipped) logger.warn(`Skipped ${file} (already exists, use --force to overwrite)`);
564
+ for (const err of result.errors) logger.error(err);
565
+ }
566
+ },
567
+ delete: {
568
+ meta: {
569
+ name: "delete",
570
+ description: "Delete a scaffold file (creates backup by default)"
571
+ },
572
+ args: {
573
+ path: {
574
+ type: "positional",
575
+ description: "Path to delete"
576
+ },
577
+ type: {
578
+ type: "string",
579
+ description: "Type: page, api, layout, middleware, reuse, cron, plugin",
580
+ default: "page"
581
+ },
582
+ force: {
583
+ type: "boolean",
584
+ description: "Delete permanently without creating backup",
585
+ default: false,
586
+ alias: "f"
587
+ },
588
+ dry: {
589
+ type: "boolean",
590
+ description: "Show what would be deleted",
591
+ default: false
592
+ }
593
+ },
594
+ async run({ args }) {
595
+ const type = args.type;
596
+ if (!allTypes.includes(type)) {
597
+ logger.error(`Invalid type: ${type}. Must be one of: ${allTypes.join(", ")}`);
598
+ return;
599
+ }
600
+ const result = await deleteScaffold({
601
+ cwd: process.cwd(),
602
+ type,
603
+ path: args.path,
604
+ force: args.force,
605
+ dry: args.dry
606
+ });
607
+ for (const file of result.deleted) logger.success(`${args.dry ? "[dry-run] Would delete" : args.force ? "Deleted" : "Deleted (backup created)"} ${file}`);
608
+ for (const err of result.errors) logger.error(err);
609
+ }
610
+ },
611
+ recovery: {
612
+ meta: {
613
+ name: "recovery",
614
+ description: "Recover a deleted file from .bak backup"
615
+ },
616
+ args: {
617
+ path: {
618
+ type: "positional",
619
+ description: "Path to recover"
620
+ },
621
+ type: {
622
+ type: "string",
623
+ description: "Type: page, api, layout, middleware, reuse, cron, plugin",
624
+ default: "page"
625
+ },
626
+ dry: {
627
+ type: "boolean",
628
+ description: "Check if recovery is possible",
629
+ default: false
630
+ }
631
+ },
632
+ async run({ args }) {
633
+ const type = args.type;
634
+ if (!allTypes.includes(type)) {
635
+ logger.error(`Invalid type: ${type}. Must be one of: ${allTypes.join(", ")}`);
636
+ return;
637
+ }
638
+ const result = await recoverScaffold({
639
+ cwd: process.cwd(),
640
+ type,
641
+ path: args.path,
642
+ dry: args.dry
643
+ });
644
+ for (const file of result.restored) logger.success(`${args.dry ? "[dry-run] Backup exists for" : "Recovered"} ${file}`);
645
+ for (const err of result.errors) logger.error(err);
646
+ }
647
+ },
648
+ list: {
649
+ meta: {
650
+ name: "list",
651
+ description: "List existing scaffoldable files"
652
+ },
653
+ args: { type: {
654
+ type: "string",
655
+ description: "Type to list: page, api, layout, middleware, reuse, cron, plugin",
656
+ default: "page"
657
+ } },
658
+ async run({ args }) {
659
+ const type = args.type;
660
+ const files = await listScaffoldableFiles(process.cwd(), type);
661
+ if (files.length === 0) {
662
+ logger.info(`No ${type} files found`);
663
+ return;
664
+ }
665
+ logger.info(`Found ${files.length} ${type}(s):`);
666
+ for (const file of files) logger.log(` ${file}`);
667
+ }
668
+ }
669
+ }
670
+ };
671
+ //#endregion
672
+ export { createFsOps as S, renderPluginTemplate as _, scaffold as a, toKebabCase as b, LAYOUT_TEMPLATE as c, PLUGIN_TEMPLATE as d, renderApiTemplate as f, renderPageTemplate as g, renderMiddlewareTemplate as h, recoverScaffold as i, MIDDLEWARE_TEMPLATE as l, renderLayoutTemplate as m, listScaffoldableFiles as n, API_TEMPLATE as o, renderCronTemplate as p, pageCommand as r, CRON_TEMPLATE as s, deleteScaffold as t, PAGE_TEMPLATE as u, renderTemplate as v, toPascalCase as x, toCamelCase as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ubean/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "CLI for ubean (ubean-next binary: dev, build, prepare, init, preview, page, env, config, devtools, api/layout/middleware/cron/plugin scaffolds)",
5
5
  "bin": {
6
6
  "ubean-next": "./bin/ubean-next.mjs"
@@ -28,15 +28,15 @@
28
28
  "consola": "^3.4.2",
29
29
  "kolorist": "^1.8.0",
30
30
  "pathe": "^2.0.3",
31
- "@ubean/app": "0.1.1",
32
- "@ubean/build": "0.1.1",
33
- "@ubean/codegen": "0.1.1",
34
- "@ubean/config": "0.1.1",
35
- "@ubean/prerender": "0.1.1",
36
- "@ubean/preset": "0.1.1",
37
- "@ubean/routing": "0.1.1",
38
- "@ubean/dev-server": "0.1.1",
39
- "@ubean/utils": "0.1.1"
31
+ "@ubean/app": "0.1.3",
32
+ "@ubean/codegen": "0.1.3",
33
+ "@ubean/config": "0.1.3",
34
+ "@ubean/build": "0.1.3",
35
+ "@ubean/dev-server": "0.1.3",
36
+ "@ubean/preset": "0.1.3",
37
+ "@ubean/prerender": "0.1.3",
38
+ "@ubean/routing": "0.1.3",
39
+ "@ubean/utils": "0.1.3"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^26.1.1",
@@ -44,7 +44,7 @@
44
44
  "vite-plus": "0.2.6"
45
45
  },
46
46
  "peerDependencies": {
47
- "@ubean/devtools": "0.1.1"
47
+ "@ubean/devtools": "0.1.3"
48
48
  },
49
49
  "peerDependenciesMeta": {
50
50
  "@ubean/devtools": {