vela 0.13.3 → 0.13.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -21,7 +21,7 @@ import pc42 from "picocolors";
21
21
  // package.json
22
22
  var package_default = {
23
23
  name: "vela",
24
- version: "0.13.3",
24
+ version: "0.13.4",
25
25
  type: "module",
26
26
  description: "A CLI for creating and updating SvelteKit projects",
27
27
  license: "MIT",
@@ -57,7 +57,7 @@ var package_default = {
57
57
  dependencies: {
58
58
  "@clack/prompts": "^1.7.0",
59
59
  "@faker-js/faker": "^10.6.0",
60
- "@velastack/patterns": "^0.2.9",
60
+ "@velastack/patterns": "^0.2.11",
61
61
  "@velastack/pocketbase-codegen": "^0.1.0",
62
62
  "annotate-json-schema": "^0.1.0",
63
63
  commander: "^13.1.0",
@@ -236,8 +236,8 @@ function stubCommand(name, description, fullName) {
236
236
  }
237
237
 
238
238
  // src/lib/workspace.ts
239
- import fs2 from "node:fs";
240
- import path from "node:path";
239
+ import fs3 from "node:fs";
240
+ import path2 from "node:path";
241
241
  import process3 from "node:process";
242
242
 
243
243
  // src/lib/constants.ts
@@ -254,8 +254,36 @@ var API_URL = (process2.env.VELA_API_URL?.trim() || "https://velastack.dev").rep
254
254
  var FIXTURE_PREFIX = "vela";
255
255
  var TEMPLATE_INDEX_URL = process2.env.VELA_TEMPLATE_INDEX_URL?.trim() || "https://templates.velastack.app/index.json";
256
256
 
257
- // src/lib/package-json.ts
257
+ // src/lib/components-json.ts
258
258
  import fs from "node:fs";
259
+ import path from "node:path";
260
+ function readComponentsJson(root) {
261
+ const file = path.join(root, "components.json");
262
+ if (!fs.existsSync(file)) return void 0;
263
+ try {
264
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
265
+ return parsed && typeof parsed === "object" ? parsed : void 0;
266
+ } catch {
267
+ return void 0;
268
+ }
269
+ }
270
+ function componentsJsonHints(config) {
271
+ const hints = [];
272
+ if (!config.style) {
273
+ hints.push(
274
+ 'components.json has no "style": shadcn-svelte defaults to "nova", while vela ships "vega". Add "style": "vega" so new components match.'
275
+ );
276
+ }
277
+ if (!config.iconLibrary) {
278
+ hints.push(
279
+ `components.json has no "iconLibrary": add "iconLibrary": "lucide", the library vela's components use.`
280
+ );
281
+ }
282
+ return hints;
283
+ }
284
+
285
+ // src/lib/package-json.ts
286
+ import fs2 from "node:fs";
259
287
  var DEP_KINDS = ["dependencies", "devDependencies"];
260
288
  function mergePackageJson(user, template) {
261
289
  const merged = structuredClone(user);
@@ -316,7 +344,7 @@ function dropTemplateAdapters(user, template) {
316
344
  return copy;
317
345
  }
318
346
  function readPackageJson(path48) {
319
- return JSON.parse(fs.readFileSync(path48, "utf8"));
347
+ return JSON.parse(fs2.readFileSync(path48, "utf8"));
320
348
  }
321
349
  var PACKAGE_NAME_PLACEHOLDER = /~TODO~/g;
322
350
  var APP_NAME_PLACEHOLDER = /~APP_NAME~/g;
@@ -335,11 +363,11 @@ function escapeSingleQuoted(value) {
335
363
  return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
336
364
  }
337
365
  function readTemplatePackageJson(path48, values) {
338
- const raw = fillTemplatePlaceholders(fs.readFileSync(path48, "utf8"), values);
366
+ const raw = fillTemplatePlaceholders(fs2.readFileSync(path48, "utf8"), values);
339
367
  return JSON.parse(raw);
340
368
  }
341
369
  function writePackageJson(path48, pkg) {
342
- fs.writeFileSync(path48, JSON.stringify(pkg, null, " ") + "\n");
370
+ fs2.writeFileSync(path48, JSON.stringify(pkg, null, " ") + "\n");
343
371
  }
344
372
  function toValidPackageName(name) {
345
373
  return name.trim().toLowerCase().replace(/\s+/g, "-").replace(/^[._]/, "").replace(/[^a-z0-9~.-]+/g, "-");
@@ -353,44 +381,40 @@ function sortKeys(obj) {
353
381
  // src/lib/workspace.ts
354
382
  function findWorkspaceRoot(from = process3.cwd()) {
355
383
  let currentDir = from;
356
- while (currentDir !== path.parse(currentDir).root) {
357
- if (fs2.existsSync(path.join(currentDir, "package.json"))) return currentDir;
358
- currentDir = path.dirname(currentDir);
384
+ while (currentDir !== path2.parse(currentDir).root) {
385
+ if (fs3.existsSync(path2.join(currentDir, "package.json"))) return currentDir;
386
+ currentDir = path2.dirname(currentDir);
359
387
  }
360
388
  return null;
361
389
  }
362
390
  function hasBackend(from = process3.cwd()) {
363
391
  const root = findWorkspaceRoot(from);
364
- return root !== null && fs2.existsSync(path.join(root, DATA_DIR));
392
+ return root !== null && fs3.existsSync(path2.join(root, DATA_DIR));
365
393
  }
366
394
  function hasApiRoutes(root) {
367
- const dir = path.join(root, "src", "routes", "api");
368
- if (!fs2.existsSync(dir)) return false;
369
- return fs2.readdirSync(dir).some((entry) => entry !== "README.md");
395
+ const dir = path2.join(root, "src", "routes", "api");
396
+ if (!fs3.existsSync(dir)) return false;
397
+ return fs3.readdirSync(dir).some((entry) => entry !== "README.md");
370
398
  }
371
399
  function localDataDir(from = process3.cwd()) {
372
- return path.join(findWorkspaceRoot(from) ?? from, DATA_DIR);
400
+ return path2.join(findWorkspaceRoot(from) ?? from, DATA_DIR);
373
401
  }
374
402
  async function getWorkspace() {
375
403
  const workspaceRootDir = findWorkspaceRoot();
376
404
  if (!workspaceRootDir) {
377
405
  throw new Error("Could not find workspace root (no package.json found)");
378
406
  }
379
- const routesDir = path.join("src", "routes");
380
- const fullRoutesPath = path.join(workspaceRootDir, routesDir);
381
- if (!fs2.existsSync(fullRoutesPath)) {
407
+ const routesDir = path2.join("src", "routes");
408
+ const fullRoutesPath = path2.join(workspaceRootDir, routesDir);
409
+ if (!fs3.existsSync(fullRoutesPath)) {
382
410
  throw new Error("Could not find src/routes directory");
383
411
  }
384
- let publicRoutesDir = path.join(routesDir, PUBLIC_DIR);
385
- if (!fs2.existsSync(path.join(workspaceRootDir, publicRoutesDir))) {
386
- publicRoutesDir = routesDir;
387
- }
388
- let appRoutesDir;
389
- const appRoutesPath = path.join(fullRoutesPath, APP_DIR);
390
- const isAppMode = fs2.existsSync(appRoutesPath);
391
- if (isAppMode) appRoutesDir = path.join(routesDir, APP_DIR);
392
- const isPaymentsMode = fs2.existsSync(
393
- path.join(workspaceRootDir, routesDir, "webhooks", "stripe")
412
+ const routeGroups = detectRouteGroups(workspaceRootDir);
413
+ const publicRoutesDir = path2.join(routesDir, routeGroups.public ?? "");
414
+ const isAppMode = routeGroups.app !== null;
415
+ const appRoutesDir = routeGroups.app ? path2.join(routesDir, routeGroups.app) : void 0;
416
+ const isPaymentsMode = fs3.existsSync(
417
+ path2.join(workspaceRootDir, routesDir, "webhooks", "stripe")
394
418
  );
395
419
  const features = detectFeatures(workspaceRootDir, { isAppMode, isPaymentsMode });
396
420
  return {
@@ -398,14 +422,25 @@ async function getWorkspace() {
398
422
  routesDir,
399
423
  publicRoutesDir,
400
424
  appRoutesDir,
425
+ routeGroups,
401
426
  isAppMode,
402
427
  isPaymentsMode,
403
428
  features
404
429
  };
405
430
  }
431
+ function detectRouteGroups(root) {
432
+ const group8 = (name) => fs3.existsSync(path2.join(root, "src", "routes", name)) ? name : null;
433
+ return { public: group8(PUBLIC_DIR), app: group8(APP_DIR) };
434
+ }
435
+ function detectUi(root) {
436
+ const pkg = readPackageJson(path2.join(root, "package.json"));
437
+ const hasDep = (name) => Boolean(pkg.dependencies?.[name] || pkg.devDependencies?.[name]);
438
+ const shadcn = readComponentsJson(root) !== void 0 && (hasDep("shadcn-svelte") || hasDep("bits-ui"));
439
+ return shadcn ? "shadcn" : "plain";
440
+ }
406
441
  function detectFeatures(root, { isAppMode, isPaymentsMode }) {
407
- const has = (rel) => fs2.existsSync(path.join(root, rel));
408
- const pkg = readPackageJson(path.join(root, "package.json"));
442
+ const has = (rel) => fs3.existsSync(path2.join(root, rel));
443
+ const pkg = readPackageJson(path2.join(root, "package.json"));
409
444
  const hasDep = (name) => Boolean(pkg.dependencies?.[name] || pkg.devDependencies?.[name]);
410
445
  return {
411
446
  auth: isAppMode,
@@ -418,7 +453,8 @@ function detectFeatures(root, { isAppMode, isPaymentsMode }) {
418
453
  blog: hasDep("mdsvex"),
419
454
  contentNegotiation: hasDep("sveltekit-negotiate"),
420
455
  cms: hasDep("@velastack/cms"),
421
- workflows: has("src/lib/server/workflows.ts")
456
+ workflows: has("src/lib/server/workflows.ts"),
457
+ ui: detectUi(root)
422
458
  };
423
459
  }
424
460
 
@@ -460,46 +496,46 @@ function toFlag(key) {
460
496
  }
461
497
 
462
498
  // src/lib/templates.ts
463
- import fs6 from "node:fs";
464
- import path5 from "node:path";
499
+ import fs7 from "node:fs";
500
+ import path6 from "node:path";
465
501
  import { fileURLToPath as fileURLToPath2 } from "node:url";
466
502
 
467
503
  // src/lib/template-files.ts
468
- import fs3 from "node:fs";
469
- import path2 from "node:path";
504
+ import fs4 from "node:fs";
505
+ import path3 from "node:path";
470
506
  var PUBLISH_SAFE_NAMES = {
471
507
  ".gitignore": "_gitignore",
472
508
  ".npmrc": "_npmrc"
473
509
  };
474
510
  function templateName(projectRelPath) {
475
- const safe = PUBLISH_SAFE_NAMES[path2.basename(projectRelPath)];
511
+ const safe = PUBLISH_SAFE_NAMES[path3.basename(projectRelPath)];
476
512
  if (!safe) return projectRelPath;
477
- const dir = path2.dirname(projectRelPath);
478
- return dir === "." ? safe : path2.join(dir, safe);
513
+ const dir = path3.dirname(projectRelPath);
514
+ return dir === "." ? safe : path3.join(dir, safe);
479
515
  }
480
516
  function restoreTemplateNames(target) {
481
517
  for (const [real, safe] of Object.entries(PUBLISH_SAFE_NAMES)) {
482
- const from = path2.join(target, safe);
483
- if (!fs3.existsSync(from)) continue;
484
- fs3.renameSync(from, path2.join(target, real));
518
+ const from = path3.join(target, safe);
519
+ if (!fs4.existsSync(from)) continue;
520
+ fs4.renameSync(from, path3.join(target, real));
485
521
  }
486
522
  }
487
523
  var TEMPLATE_SOURCE = /\.template\.([^.]+)$/;
488
524
  function applyTemplateFiles(target, values) {
489
525
  const written = [];
490
526
  for (const source of findTemplateSources(target)) {
491
- const raw = fs3.readFileSync(source, "utf8");
527
+ const raw = fs4.readFileSync(source, "utf8");
492
528
  const dest = source.replace(TEMPLATE_SOURCE, ".$1");
493
- fs3.writeFileSync(dest, fillTemplatePlaceholders(raw, values));
494
- fs3.unlinkSync(source);
495
- written.push(path2.relative(target, dest));
529
+ fs4.writeFileSync(dest, fillTemplatePlaceholders(raw, values));
530
+ fs4.unlinkSync(source);
531
+ written.push(path3.relative(target, dest));
496
532
  }
497
533
  return written.sort();
498
534
  }
499
535
  function findTemplateSources(dir) {
500
536
  const found = [];
501
- for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
502
- const full = path2.join(dir, entry.name);
537
+ for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
538
+ const full = path3.join(dir, entry.name);
503
539
  if (entry.isDirectory()) {
504
540
  if (entry.name === "node_modules" || entry.name === ".git") continue;
505
541
  found.push(...findTemplateSources(full));
@@ -511,38 +547,38 @@ function findTemplateSources(dir) {
511
547
  }
512
548
 
513
549
  // src/lib/template-registry.ts
514
- import fs5 from "node:fs";
550
+ import fs6 from "node:fs";
515
551
  import os2 from "node:os";
516
- import path4 from "node:path";
552
+ import path5 from "node:path";
517
553
  import { createHash } from "node:crypto";
518
554
  import { fileURLToPath } from "node:url";
519
555
  import * as v2 from "valibot";
520
556
  import * as tar from "tar";
521
557
 
522
558
  // src/lib/config.ts
523
- import fs4 from "node:fs";
559
+ import fs5 from "node:fs";
524
560
  import os from "node:os";
525
- import path3 from "node:path";
561
+ import path4 from "node:path";
526
562
  import process4 from "node:process";
527
563
  function configDir() {
528
- return path3.join(os.homedir(), ".vela");
564
+ return path4.join(os.homedir(), ".vela");
529
565
  }
530
566
  var CONFIG_DIR = configDir();
531
- var CONFIG_PATH = path3.join(CONFIG_DIR, "config.json");
567
+ var CONFIG_PATH = path4.join(CONFIG_DIR, "config.json");
532
568
  function readConfig() {
533
- if (!fs4.existsSync(CONFIG_PATH)) return null;
569
+ if (!fs5.existsSync(CONFIG_PATH)) return null;
534
570
  try {
535
- return JSON.parse(fs4.readFileSync(CONFIG_PATH, "utf8"));
571
+ return JSON.parse(fs5.readFileSync(CONFIG_PATH, "utf8"));
536
572
  } catch {
537
573
  return null;
538
574
  }
539
575
  }
540
576
  function writeConfig(config) {
541
- fs4.mkdirSync(CONFIG_DIR, { recursive: true });
542
- fs4.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
577
+ fs5.mkdirSync(CONFIG_DIR, { recursive: true });
578
+ fs5.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
543
579
  }
544
580
  function clearConfig() {
545
- if (fs4.existsSync(CONFIG_PATH)) fs4.unlinkSync(CONFIG_PATH);
581
+ if (fs5.existsSync(CONFIG_PATH)) fs5.unlinkSync(CONFIG_PATH);
546
582
  }
547
583
  function readApiKey() {
548
584
  const fromEnv = process4.env.VELA_API_KEY?.trim();
@@ -588,16 +624,16 @@ var indexSchema = v2.object({
588
624
  templates: v2.array(v2.unknown())
589
625
  });
590
626
  function cacheRoot() {
591
- return path4.join(configDir(), CACHE_DIR_NAME);
627
+ return path5.join(configDir(), CACHE_DIR_NAME);
592
628
  }
593
629
  function indexCachePath() {
594
- return path4.join(cacheRoot(), INDEX_CACHE_FILE);
630
+ return path5.join(cacheRoot(), INDEX_CACHE_FILE);
595
631
  }
596
632
  function readCachedIndex(url) {
597
633
  const file = indexCachePath();
598
- if (!fs5.existsSync(file)) return null;
634
+ if (!fs6.existsSync(file)) return null;
599
635
  try {
600
- const cached2 = JSON.parse(fs5.readFileSync(file, "utf8"));
636
+ const cached2 = JSON.parse(fs6.readFileSync(file, "utf8"));
601
637
  if (cached2.url !== url) return null;
602
638
  return cached2;
603
639
  } catch {
@@ -605,8 +641,8 @@ function readCachedIndex(url) {
605
641
  }
606
642
  }
607
643
  function writeCachedIndex(cached2) {
608
- fs5.mkdirSync(cacheRoot(), { recursive: true });
609
- fs5.writeFileSync(indexCachePath(), JSON.stringify(cached2, null, 2));
644
+ fs6.mkdirSync(cacheRoot(), { recursive: true });
645
+ fs6.writeFileSync(indexCachePath(), JSON.stringify(cached2, null, 2));
610
646
  }
611
647
  function parseTemplateIndex(text19, url) {
612
648
  let parsed;
@@ -633,7 +669,7 @@ function isFileUrl(url) {
633
669
  }
634
670
  async function readBytes(url, options) {
635
671
  if (isFileUrl(url)) {
636
- return new Uint8Array(fs5.readFileSync(fileURLToPath(url)));
672
+ return new Uint8Array(fs6.readFileSync(fileURLToPath(url)));
637
673
  }
638
674
  const res = await fetch(url, {
639
675
  headers: options.headers,
@@ -671,7 +707,7 @@ function sha256(bytes) {
671
707
  return createHash("sha256").update(bytes).digest("hex");
672
708
  }
673
709
  function tarballCachePath(entry) {
674
- return path4.join(
710
+ return path5.join(
675
711
  cacheRoot(),
676
712
  TARBALL_CACHE_DIR,
677
713
  `${entry.name}-${entry.sha256.slice(0, 12)}.tgz`
@@ -681,8 +717,8 @@ async function downloadTemplate(entry, indexUrl, options = {}) {
681
717
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS * 6;
682
718
  const cacheFile = tarballCachePath(entry);
683
719
  let haveTarball = false;
684
- if (fs5.existsSync(cacheFile)) {
685
- const cached2 = new Uint8Array(fs5.readFileSync(cacheFile));
720
+ if (fs6.existsSync(cacheFile)) {
721
+ const cached2 = new Uint8Array(fs6.readFileSync(cacheFile));
686
722
  haveTarball = sha256(cached2) === entry.sha256;
687
723
  }
688
724
  if (!haveTarball) {
@@ -699,14 +735,14 @@ async function downloadTemplate(entry, indexUrl, options = {}) {
699
735
  `Downloaded template ${entry.name} did not match its checksum (expected ${entry.sha256}, got ${digest}). Try again, or set VELA_TEMPLATE_INDEX_URL to a registry you trust.`
700
736
  );
701
737
  }
702
- fs5.mkdirSync(path4.dirname(cacheFile), { recursive: true });
703
- fs5.writeFileSync(cacheFile, fetched);
738
+ fs6.mkdirSync(path5.dirname(cacheFile), { recursive: true });
739
+ fs6.writeFileSync(cacheFile, fetched);
704
740
  }
705
- const dir = fs5.mkdtempSync(path4.join(os2.tmpdir(), `vela-template-${entry.name}-`));
741
+ const dir = fs6.mkdtempSync(path5.join(os2.tmpdir(), `vela-template-${entry.name}-`));
706
742
  try {
707
743
  await tar.extract({ file: cacheFile, cwd: dir, strip: 1 });
708
744
  } catch (e) {
709
- fs5.rmSync(dir, { recursive: true, force: true });
745
+ fs6.rmSync(dir, { recursive: true, force: true });
710
746
  throw new Error(`Could not unpack template ${entry.name}: ${e.message}`);
711
747
  }
712
748
  return dir;
@@ -718,25 +754,25 @@ var DEFAULT_TEMPLATE = "minimal";
718
754
  var DEFAULT_CATEGORY = "starter";
719
755
  var cached;
720
756
  function templatesDir() {
721
- let dir = path5.dirname(fileURLToPath2(import.meta.url));
722
- const { root } = path5.parse(dir);
757
+ let dir = path6.dirname(fileURLToPath2(import.meta.url));
758
+ const { root } = path6.parse(dir);
723
759
  while (dir !== root) {
724
- const candidate = path5.join(dir, "templates");
760
+ const candidate = path6.join(dir, "templates");
725
761
  if (holdsManifest(candidate)) return candidate;
726
- dir = path5.dirname(dir);
762
+ dir = path6.dirname(dir);
727
763
  }
728
764
  throw new Error("Could not locate the templates directory");
729
765
  }
730
766
  function holdsManifest(dir) {
731
- if (!fs6.existsSync(dir)) return false;
732
- return fs6.readdirSync(dir, { withFileTypes: true }).some(
733
- (entry) => entry.isDirectory() && fs6.existsSync(path5.join(dir, entry.name, TEMPLATE_MANIFEST))
767
+ if (!fs7.existsSync(dir)) return false;
768
+ return fs7.readdirSync(dir, { withFileTypes: true }).some(
769
+ (entry) => entry.isDirectory() && fs7.existsSync(path6.join(dir, entry.name, TEMPLATE_MANIFEST))
734
770
  );
735
771
  }
736
772
  function listProjectTemplates() {
737
773
  if (cached) return cached;
738
774
  const root = templatesDir();
739
- cached = fs6.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => readManifest(root, entry.name)).filter((template) => template !== void 0).sort((a, b) => a.name.localeCompare(b.name));
775
+ cached = fs7.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => readManifest(root, entry.name)).filter((template) => template !== void 0).sort((a, b) => a.name.localeCompare(b.name));
740
776
  return cached;
741
777
  }
742
778
  function projectTemplateNames(options = {}) {
@@ -809,25 +845,25 @@ async function resolveTemplate(template) {
809
845
  ...info,
810
846
  dir,
811
847
  cleanup() {
812
- fs6.rmSync(dir, { recursive: true, force: true });
848
+ fs7.rmSync(dir, { recursive: true, force: true });
813
849
  }
814
850
  };
815
851
  }
816
852
  function copyTemplate(template, target) {
817
- fs6.mkdirSync(target, { recursive: true });
818
- fs6.cpSync(template.dir, target, {
853
+ fs7.mkdirSync(target, { recursive: true });
854
+ fs7.cpSync(template.dir, target, {
819
855
  recursive: true,
820
856
  // The manifest describes the template to the CLI; it isn't part of the project.
821
- filter: (src) => path5.basename(src) !== ".DS_Store" && path5.relative(template.dir, src) !== TEMPLATE_MANIFEST
857
+ filter: (src) => path6.basename(src) !== ".DS_Store" && path6.relative(template.dir, src) !== TEMPLATE_MANIFEST
822
858
  });
823
859
  restoreTemplateNames(target);
824
860
  }
825
861
  function readManifest(root, name) {
826
- const manifestPath = path5.join(root, name, TEMPLATE_MANIFEST);
827
- if (!fs6.existsSync(manifestPath)) return void 0;
862
+ const manifestPath = path6.join(root, name, TEMPLATE_MANIFEST);
863
+ if (!fs7.existsSync(manifestPath)) return void 0;
828
864
  let parsed;
829
865
  try {
830
- parsed = JSON.parse(fs6.readFileSync(manifestPath, "utf8"));
866
+ parsed = JSON.parse(fs7.readFileSync(manifestPath, "utf8"));
831
867
  } catch (e) {
832
868
  throw new Error(
833
869
  `Template ${name} has an unreadable ${TEMPLATE_MANIFEST}: ${e.message}`
@@ -850,7 +886,7 @@ function readManifest(root, name) {
850
886
  price: typeof manifest.price === "number" ? manifest.price : void 0,
851
887
  nextSteps: optionalStringArray(manifest.nextSteps),
852
888
  cms: typeof manifest.cms === "boolean" ? manifest.cms : void 0,
853
- dir: path5.join(root, name)
889
+ dir: path6.join(root, name)
854
890
  };
855
891
  }
856
892
  function optionalString(value) {
@@ -863,8 +899,8 @@ function optionalStringArray(value) {
863
899
  }
864
900
 
865
901
  // src/lib/package-manager.ts
866
- import fs7 from "node:fs";
867
- import path6 from "node:path";
902
+ import fs8 from "node:fs";
903
+ import path7 from "node:path";
868
904
  import process5 from "node:process";
869
905
  import { exec } from "tinyexec";
870
906
  import { Option } from "commander";
@@ -934,9 +970,9 @@ async function installDependencies(agent, cwd, { exitOnFailure = true } = {}) {
934
970
  }
935
971
  function addPnpmBuildDependencies(cwd, packageManager2, allowedPackages) {
936
972
  if (!packageManager2 || packageManager2 !== "pnpm") return;
937
- const pkgPath = path6.join(cwd, "package.json");
938
- if (!fs7.existsSync(pkgPath)) return;
939
- const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
973
+ const pkgPath = path7.join(cwd, "package.json");
974
+ if (!fs8.existsSync(pkgPath)) return;
975
+ const pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
940
976
  pkg.pnpm ??= {};
941
977
  pkg.pnpm.onlyBuiltDependencies ??= [];
942
978
  for (const name of allowedPackages) {
@@ -944,13 +980,13 @@ function addPnpmBuildDependencies(cwd, packageManager2, allowedPackages) {
944
980
  pkg.pnpm.onlyBuiltDependencies.push(name);
945
981
  }
946
982
  }
947
- fs7.writeFileSync(pkgPath, JSON.stringify(pkg, null, " ") + "\n");
983
+ fs8.writeFileSync(pkgPath, JSON.stringify(pkg, null, " ") + "\n");
948
984
  }
949
985
 
950
986
  // src/lib/pocketbase.ts
951
- import fs8 from "node:fs";
987
+ import fs9 from "node:fs";
952
988
  import net from "node:net";
953
- import path7 from "node:path";
989
+ import path8 from "node:path";
954
990
  import process6 from "node:process";
955
991
  import { createRequire } from "node:module";
956
992
  import { spawn } from "node:child_process";
@@ -1063,9 +1099,9 @@ async function authWithRetries(pb, email3, password11, attempts = 3) {
1063
1099
  }
1064
1100
  }
1065
1101
  function getPocketbaseMetadata(cwd) {
1066
- const metadataPath = path7.join(cwd, "node_modules", ".vite", "_pocketbase_metadata.json");
1067
- if (fs8.existsSync(metadataPath)) {
1068
- return JSON.parse(fs8.readFileSync(metadataPath, "utf8"));
1102
+ const metadataPath = path8.join(cwd, "node_modules", ".vite", "_pocketbase_metadata.json");
1103
+ if (fs9.existsSync(metadataPath)) {
1104
+ return JSON.parse(fs9.readFileSync(metadataPath, "utf8"));
1069
1105
  }
1070
1106
  return null;
1071
1107
  }
@@ -1078,12 +1114,12 @@ async function execPackageBin(cwd, args, stdio = "pipe") {
1078
1114
  return x(command, resolvedArgs, { nodeOptions: { cwd, stdio }, throwOnError: true });
1079
1115
  }
1080
1116
  async function withPocketbase(cwd, fn, creds) {
1081
- const dir = path7.join(cwd, DATA_DIR);
1082
- const migrationsDir = path7.join(cwd, MIGRATIONS_DIR);
1117
+ const dir = path8.join(cwd, DATA_DIR);
1118
+ const migrationsDir = path8.join(cwd, MIGRATIONS_DIR);
1083
1119
  const host = "localhost";
1084
1120
  const email3 = creds?.email ?? process6.env.POCKETBASE_SUPERUSER_EMAIL;
1085
1121
  const password11 = creds?.password ?? process6.env.POCKETBASE_SUPERUSER_PASSWORD;
1086
- if (!fs8.existsSync(dir)) {
1122
+ if (!fs9.existsSync(dir)) {
1087
1123
  throw new Error("PocketBase data directory does not exist");
1088
1124
  }
1089
1125
  const metadata = getPocketbaseMetadata(cwd);
@@ -1108,10 +1144,10 @@ async function withPocketbase(cwd, fn, creds) {
1108
1144
  }
1109
1145
  }
1110
1146
  async function createSuperuser(cwd, email3, password11) {
1111
- const dir = path7.join(cwd, DATA_DIR);
1112
- const migrationsDir = path7.join(cwd, MIGRATIONS_DIR);
1113
- fs8.mkdirSync(dir, { recursive: true });
1114
- fs8.mkdirSync(migrationsDir, { recursive: true });
1147
+ const dir = path8.join(cwd, DATA_DIR);
1148
+ const migrationsDir = path8.join(cwd, MIGRATIONS_DIR);
1149
+ fs9.mkdirSync(dir, { recursive: true });
1150
+ fs9.mkdirSync(migrationsDir, { recursive: true });
1115
1151
  await execPackageBin(
1116
1152
  cwd,
1117
1153
  [
@@ -1136,8 +1172,8 @@ async function launchPocketbase(cwd, {
1136
1172
  password: password11
1137
1173
  }) {
1138
1174
  const host = "localhost";
1139
- fs8.mkdirSync(dir, { recursive: true });
1140
- fs8.mkdirSync(migrationsDir, { recursive: true });
1175
+ fs9.mkdirSync(dir, { recursive: true });
1176
+ fs9.mkdirSync(migrationsDir, { recursive: true });
1141
1177
  await execPackageBin(
1142
1178
  cwd,
1143
1179
  [
@@ -1179,8 +1215,8 @@ async function ensureSuperuser(cwd) {
1179
1215
  const email3 = process6.env.POCKETBASE_SUPERUSER_EMAIL;
1180
1216
  const password11 = process6.env.POCKETBASE_SUPERUSER_PASSWORD;
1181
1217
  if (!email3 || !password11) return;
1182
- const dir = path7.join(cwd, DATA_DIR);
1183
- if (!fs8.existsSync(dir)) return;
1218
+ const dir = path8.join(cwd, DATA_DIR);
1219
+ if (!fs9.existsSync(dir)) return;
1184
1220
  const { getBinaryPath } = await import("pocketbase-server");
1185
1221
  await x(
1186
1222
  getBinaryPath(),
@@ -1188,7 +1224,7 @@ async function ensureSuperuser(cwd) {
1188
1224
  "--dir",
1189
1225
  dir,
1190
1226
  "--migrationsDir",
1191
- path7.join(cwd, MIGRATIONS_DIR),
1227
+ path8.join(cwd, MIGRATIONS_DIR),
1192
1228
  "superuser",
1193
1229
  "upsert",
1194
1230
  email3,
@@ -1199,8 +1235,8 @@ async function ensureSuperuser(cwd) {
1199
1235
  }
1200
1236
 
1201
1237
  // src/lib/env.ts
1202
- import fs9 from "node:fs";
1203
- import path8 from "node:path";
1238
+ import fs10 from "node:fs";
1239
+ import path9 from "node:path";
1204
1240
  function addEnvVar(content, key, value) {
1205
1241
  if (content.includes(`${key}=`)) return content;
1206
1242
  return appendLine(content, `${key}=${value}`);
@@ -1215,11 +1251,11 @@ function appendLine(existing, line) {
1215
1251
  return withNewline + line + "\n";
1216
1252
  }
1217
1253
  function writeEnvFile(cwd, vars, comments = []) {
1218
- const envPath = path8.join(cwd, ".env");
1219
- let content = fs9.existsSync(envPath) ? fs9.readFileSync(envPath, "utf8") : "";
1254
+ const envPath = path9.join(cwd, ".env");
1255
+ let content = fs10.existsSync(envPath) ? fs10.readFileSync(envPath, "utf8") : "";
1220
1256
  for (const comment of comments) content = addEnvComment(content, comment);
1221
1257
  for (const [key, value] of Object.entries(vars)) content = addEnvVar(content, key, value);
1222
- fs9.writeFileSync(envPath, content);
1258
+ fs10.writeFileSync(envPath, content);
1223
1259
  }
1224
1260
  function upsertEnvVar(content, key, value) {
1225
1261
  const line = `${key}=${quoteEnvValue(value)}`;
@@ -1252,7 +1288,7 @@ function quoteEnvValue(value) {
1252
1288
  }
1253
1289
 
1254
1290
  // src/lib/app-css.ts
1255
- import fs10 from "node:fs";
1291
+ import fs11 from "node:fs";
1256
1292
  var SHADCN_TAILWIND_CSS = "shadcn-svelte/tailwind.css";
1257
1293
  var SHADCN_TAILWIND_IMPORT = `@import '${SHADCN_TAILWIND_CSS}';`;
1258
1294
  function hasShadcnImport(css) {
@@ -1278,41 +1314,13 @@ ${css}`;
1278
1314
  return lines.join("\n");
1279
1315
  }
1280
1316
  function ensureShadcnImport(appCssPath) {
1281
- if (!fs10.existsSync(appCssPath)) return false;
1282
- const css = fs10.readFileSync(appCssPath, "utf8");
1317
+ if (!fs11.existsSync(appCssPath)) return false;
1318
+ const css = fs11.readFileSync(appCssPath, "utf8");
1283
1319
  if (hasShadcnImport(css)) return false;
1284
- fs10.writeFileSync(appCssPath, insertShadcnImport(css));
1320
+ fs11.writeFileSync(appCssPath, insertShadcnImport(css));
1285
1321
  return true;
1286
1322
  }
1287
1323
 
1288
- // src/lib/components-json.ts
1289
- import fs11 from "node:fs";
1290
- import path9 from "node:path";
1291
- function readComponentsJson(root) {
1292
- const file = path9.join(root, "components.json");
1293
- if (!fs11.existsSync(file)) return void 0;
1294
- try {
1295
- const parsed = JSON.parse(fs11.readFileSync(file, "utf8"));
1296
- return parsed && typeof parsed === "object" ? parsed : void 0;
1297
- } catch {
1298
- return void 0;
1299
- }
1300
- }
1301
- function componentsJsonHints(config) {
1302
- const hints = [];
1303
- if (!config.style) {
1304
- hints.push(
1305
- 'components.json has no "style": shadcn-svelte defaults to "nova", while vela ships "vega". Add "style": "vega" so new components match.'
1306
- );
1307
- }
1308
- if (!config.iconLibrary) {
1309
- hints.push(
1310
- `components.json has no "iconLibrary": add "iconLibrary": "lucide", the library vela's components use.`
1311
- );
1312
- }
1313
- return hints;
1314
- }
1315
-
1316
1324
  // src/lib/config-merge.ts
1317
1325
  import fs13 from "node:fs";
1318
1326
  import path11 from "node:path";
@@ -2691,7 +2699,7 @@ async function runPattern(slug2, argv, input, report4) {
2691
2699
  throw new Error(`Unknown pattern: ${slug2}`);
2692
2700
  }
2693
2701
  checkProviderInput(pattern, argv, input);
2694
- const { workspaceRootDir, features } = await getWorkspace();
2702
+ const { workspaceRootDir, features, routeGroups } = await getWorkspace();
2695
2703
  const log50 = p9.taskLog({ title: report4.task.title });
2696
2704
  let result;
2697
2705
  try {
@@ -2700,6 +2708,7 @@ async function runPattern(slug2, argv, input, report4) {
2700
2708
  env: "runtime",
2701
2709
  root: workspaceRootDir,
2702
2710
  features,
2711
+ routeGroups,
2703
2712
  input,
2704
2713
  // Patterns no longer read the schema themselves: @velastack/pocketbase-codegen
2705
2714
  // takes an injected client, and only the CLI knows how to reach (or spawn)
@@ -2751,24 +2760,22 @@ async function runPattern(slug2, argv, input, report4) {
2751
2760
 
2752
2761
  // src/lib/form-ui.ts
2753
2762
  import path17 from "node:path";
2754
- var FORM_UIS = ["shadcn", "plain"];
2763
+ var UIS = ["shadcn", "plain"];
2755
2764
  function detectFormInput(root) {
2756
2765
  const pkg = readPackageJson(path17.join(root, "package.json"));
2757
2766
  const hasDep = (name) => Boolean(pkg.dependencies?.[name] || pkg.devDependencies?.[name]);
2758
- const shadcn = readComponentsJson(root) !== void 0 && (hasDep("shadcn-svelte") || hasDep("bits-ui"));
2759
2767
  return {
2760
- ui: shadcn ? "shadcn" : "plain",
2761
2768
  flash: hasDep("sveltekit-flash-message"),
2762
2769
  serverTests: hasDep("supertest")
2763
2770
  };
2764
2771
  }
2765
- function resolveFormInput(root, requested) {
2772
+ function resolveFormInput(root, detectedUi, requested) {
2766
2773
  const detected = detectFormInput(root);
2767
2774
  if (requested === void 0) return detected;
2768
- if (!FORM_UIS.includes(requested)) {
2769
- throw new Error(`Unknown --ui "${requested}". Expected one of: ${FORM_UIS.join(", ")}.`);
2775
+ if (!UIS.includes(requested)) {
2776
+ throw new Error(`Unknown --ui "${requested}". Expected one of: ${UIS.join(", ")}.`);
2770
2777
  }
2771
- if (requested === "shadcn" && detected.ui !== "shadcn") {
2778
+ if (requested === "shadcn" && detectedUi !== "shadcn") {
2772
2779
  throw new Error(
2773
2780
  "--ui shadcn needs a shadcn-svelte project (a components.json and the shadcn-svelte package). Run `vela bless` to set one up, or use --ui plain."
2774
2781
  );
@@ -3063,7 +3070,7 @@ function writeLayoutSidecar(workspaceRootDir, modelName, layout) {
3063
3070
  // src/commands/generate/form.ts
3064
3071
  var form = new Command5("form").description("generate a form from a model").argument("[model]", 'model name (e.g. "contact")').argument("[fields...]", 'field definitions (e.g. "name:text", "email:email")').option("--remote", "generate a form backed by a remote PocketBase collection").option(
3065
3072
  "--route <route>",
3066
- 'place the form at a custom route (e.g. "(app)/[team_id]/projects/new"). Defaults to the model name under (app)/(public).'
3073
+ 'place the form at a custom route (e.g. "(app)/[team_id]/projects/new"). Defaults to the model name under the (app) or (public) group, or src/routes when it has neither.'
3067
3074
  ).option(
3068
3075
  "--ui <ui>",
3069
3076
  'markup to generate: "shadcn" components or "plain" HTML. Defaults to shadcn when the project has shadcn-svelte, plain otherwise.'
@@ -3099,9 +3106,9 @@ var form = new Command5("form").description("generate a form from a model").argu
3099
3106
  argv = [model, ...fields];
3100
3107
  modelName = model;
3101
3108
  }
3102
- const { workspaceRootDir } = await getWorkspace();
3103
- const formInput = resolveFormInput(workspaceRootDir, options.ui);
3104
- if (formInput.ui === "plain" && !options.ui) {
3109
+ const { workspaceRootDir, features } = await getWorkspace();
3110
+ const formInput = resolveFormInput(workspaceRootDir, features.ui, options.ui);
3111
+ if (features.ui === "plain" && !options.ui) {
3105
3112
  p14.log.info("shadcn-svelte not detected: generating a plain HTML form.");
3106
3113
  }
3107
3114
  const slug2 = options.remote ? "generate-form-remote" : "generate-form";
@@ -3207,7 +3214,7 @@ import { Command as Command8 } from "commander";
3207
3214
  import * as p16 from "@clack/prompts";
3208
3215
  var scaffold = new Command8("scaffold").description("generate a full CRUD scaffold (model, forms, list, detail)").argument("[model]", "model name").argument("[fields...]", "field definitions").option("--remote", "generate a scaffold backed by a remote PocketBase collection").option(
3209
3216
  "--route <route>",
3210
- 'place the scaffold at a custom route (e.g. "(app)/[team_id]/projects"). Defaults to the pluralized model name under (app)/(public).'
3217
+ 'place the scaffold at a custom route (e.g. "(app)/[team_id]/projects"). Defaults to the pluralized model name under the (app) or (public) group, or src/routes when it has neither.'
3211
3218
  ).option(
3212
3219
  "--ai <description>",
3213
3220
  "design the scaffold with AI from a natural-language description (two stages: schema \u2192 layout)"