centoui-cli 1.0.0-alpha.36 → 1.0.0-alpha.38

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/index.d.mts CHANGED
@@ -16,7 +16,12 @@ type GlobalsRegistry = {
16
16
  */
17
17
  type ComponentRegistry = {
18
18
  /** Unique identifier for the component. Matches its registry filename (e.g. `"button"`). */name: string; /** Short human-readable description shown in CLI output. */
19
- description?: string;
19
+ description: string;
20
+ /**
21
+ * Whether this component requires the global utils file (e.g., isSlotEmpty).
22
+ * If true, the utils file will be written when this component is installed.
23
+ */
24
+ needsUtils?: boolean;
20
25
  /**
21
26
  * Source file paths relative to `packages/core/src/`.
22
27
  * All paths start with `components/` by convention (e.g. `"components/button/button.vue"`).
@@ -27,13 +32,13 @@ type ComponentRegistry = {
27
32
  * Names of other CentoUI components that must be installed alongside this one.
28
33
  * The CLI resolves the full dependency tree automatically.
29
34
  */
30
- componentDeps: string[];
35
+ componentDeps?: string[];
31
36
  /**
32
37
  * NPM packages required specifically by this component,
33
38
  * in addition to the global dependencies defined in `GlobalsRegistry`.
34
39
  * Keys are package names; values are semver version ranges.
35
40
  */
36
- packageDeps: Record<string, string>;
41
+ packageDeps?: Record<string, string>;
37
42
  };
38
43
  /**
39
44
  * The complete CentoUI registry — the `index.json` file fetched from GitHub.
package/dist/index.mjs CHANGED
@@ -8,7 +8,7 @@ import { loadConfig } from "c12";
8
8
  //#endregion
9
9
  //#region src/constants.ts
10
10
  /** CentoUI current package version, sourced directly from package.json. */
11
- const VERSION = "1.0.0-alpha.36";
11
+ const VERSION = "1.0.0-alpha.38";
12
12
  /** File name for the user-side CentoUI config (created by `centoui init`). */
13
13
  const CONFIG_FILE_NAME = "centoui.config.ts";
14
14
  /**
@@ -68,7 +68,7 @@ async function installMissingPackages(requiredPackages, cwd, onProgress) {
68
68
  ...packageJson.dependencies,
69
69
  ...packageJson.devDependencies
70
70
  };
71
- const packagesToInstall = Object.entries(requiredPackages).filter(([name, version]) => alreadyInstalled[name] !== version).map(([name, version]) => `${name}@${version}`);
71
+ const packagesToInstall = Object.entries(requiredPackages).filter(([name]) => !(name in alreadyInstalled)).map(([name, version]) => `${name}@${version}`);
72
72
  if (packagesToInstall.length === 0) return "All packages already up to date";
73
73
  try {
74
74
  for (const [index, pkg] of packagesToInstall.entries()) {
@@ -128,16 +128,16 @@ function validateNonEmptyPath(value) {
128
128
  //#endregion
129
129
  //#region src/utils/file-system-utils.ts
130
130
  /**
131
- * Converts a registry-relative file path into the absolute destination path
131
+ * Converts a registry-relative component file path into the absolute destination path
132
132
  * inside the user's project.
133
133
  *
134
- * Registry file paths always begin with `components/` (e.g.
134
+ * Registry component file paths always begin with `components/` (e.g.
135
135
  * `"components/button/button.vue"`). This function strips that leading segment
136
136
  * and joins the remainder with the user's configured components directory so
137
137
  * that `"components/button/button.vue"` becomes, for example,
138
138
  * `"/home/user/my-app/src/components/centoui/button/button.vue"`.
139
139
  *
140
- * @param registryFilePath - Path as it appears in the component's registry entry
140
+ * @param registryComponentFilePath - Path as it appears in the component's registry entry
141
141
  * (always starts with `"components/"`).
142
142
  * @param config - The loaded CentoUI project configuration.
143
143
  * @param cwd - Absolute path to the project root.
@@ -145,11 +145,11 @@ function validateNonEmptyPath(value) {
145
145
  *
146
146
  * @example
147
147
  * // config.componentsDir = 'src/components/centoui', cwd = '/home/user/my-app'
148
- * mapRegistryPathToProjectDest('components/button/button.vue', config, cwd)
148
+ * mapComponentsRegistryPathToProjectDest('components/button/button.vue', config, cwd)
149
149
  * // → '/home/user/my-app/src/components/centoui/button/button.vue'
150
150
  */
151
- function mapRegistryPathToProjectDest(registryFilePath, config, cwd) {
152
- const pathWithoutRegistryPrefix = registryFilePath.replace(/^components\//, "");
151
+ function mapComponentsRegistryPathToProjectDest(registryComponentFilePath, config, cwd) {
152
+ const pathWithoutRegistryPrefix = registryComponentFilePath.replace(/^components\//, "");
153
153
  return join(cwd, config.componentsDir, pathWithoutRegistryPrefix);
154
154
  }
155
155
  /**
@@ -185,7 +185,10 @@ async function writeFileWithDirs(filePath, content) {
185
185
  */
186
186
  async function confirmOverwriteIfExists(label, path) {
187
187
  if (!await fsExtra.pathExists(path)) return true;
188
- const answer = await confirm({ message: `"${label}" already exists. Overwrite?` });
188
+ const answer = await confirm({
189
+ message: `"${label}" already exists. Overwrite?`,
190
+ initialValue: false
191
+ });
189
192
  if (isCancel(answer)) {
190
193
  cancel("Operation cancelled.");
191
194
  process.exit(0);
@@ -337,7 +340,7 @@ function resolveComponentWithDependencies(componentName, registry, visited = /*
337
340
  const entry = registry.components.find((component) => component.name === componentName);
338
341
  if (!entry) throw new Error(`[resolveComponentWithDependencies] Component "${componentName}" not found in registry.`);
339
342
  result.set(componentName, entry);
340
- for (const dep of entry.componentDeps) for (const [depName, depEntry] of resolveComponentWithDependencies(dep, registry, visited)) result.set(depName, depEntry);
343
+ for (const dep of entry?.componentDeps || []) for (const [depName, depEntry] of resolveComponentWithDependencies(dep, registry, visited)) result.set(depName, depEntry);
341
344
  return result;
342
345
  }
343
346
  /**
@@ -410,9 +413,8 @@ async function fetchUtilsFileContent() {
410
413
  * Bootstraps a new CentoUI project in the current working directory.
411
414
  *
412
415
  * Flow:
413
- * 1. Prompt the user for the components directory and theme CSS file path.
414
- * 2. Ask upfront whether to overwrite any of the three output paths
415
- * (config file, theme CSS, components directory) if they already exist.
416
+ * 1. Prompt the user for the components directory, theme CSS file path, and utils file path.
417
+ * 2. Ask upfront whether to overwrite the config file, theme CSS, components directory paths if they already exist.
416
418
  * 3. Write the config file, fetch and write the theme CSS, prepare the
417
419
  * components directory, and install global npm dependencies.
418
420
  */
@@ -438,7 +440,7 @@ function init() {
438
440
  validate: validateNonEmptyPath
439
441
  }),
440
442
  utilsFilePath: () => text({
441
- message: "Path for the utils file",
443
+ message: "Path for the utils file (written on demand)",
442
444
  initialValue: "src/utils/centoui-utils.ts",
443
445
  validate: validateNonEmptyPath
444
446
  })
@@ -449,19 +451,16 @@ function init() {
449
451
  const configPath = join(cwd, CONFIG_FILE_NAME);
450
452
  const themePath = join(cwd, directories.themeFilePath);
451
453
  const componentsPath = join(cwd, directories.componentDir);
452
- const utilsPath = join(cwd, directories.utilsFilePath);
453
454
  const shouldWriteConfig = await confirmOverwriteIfExists(CONFIG_FILE_NAME, configPath);
454
455
  const shouldWriteTheme = await confirmOverwriteIfExists(directories.themeFilePath, themePath);
455
456
  const shouldWriteComponentsDir = await confirmOverwriteIfExists(directories.componentDir, componentsPath);
456
- const shouldWriteUtils = await confirmOverwriteIfExists(directories.utilsFilePath, utilsPath);
457
457
  let registry;
458
458
  await tasks([
459
459
  {
460
460
  title: "Fetching config defaults",
461
461
  task: async () => {
462
462
  if (!shouldWriteConfig) return `Skipped — "${CONFIG_FILE_NAME}" already exists`;
463
- const userConfigContent = await buildUserDefaultConfigFileContent(directories.themeFilePath, directories.componentDir, directories.utilsFilePath);
464
- await fsExtra.outputFile(configPath, userConfigContent, "utf-8");
463
+ await writeFileWithDirs(configPath, await buildUserDefaultConfigFileContent(directories.themeFilePath, directories.componentDir, directories.utilsFilePath));
465
464
  return `${CONFIG_FILE_NAME} written`;
466
465
  }
467
466
  },
@@ -469,20 +468,10 @@ function init() {
469
468
  title: "Fetching theme CSS",
470
469
  task: async () => {
471
470
  if (!shouldWriteTheme) return `Skipped — "${directories.themeFilePath}" already exists`;
472
- const themeContent = await fetchThemeCSSContent();
473
- await fsExtra.outputFile(themePath, themeContent, "utf-8");
471
+ await writeFileWithDirs(themePath, await fetchThemeCSSContent());
474
472
  return `${directories.themeFilePath} written`;
475
473
  }
476
474
  },
477
- {
478
- title: "Writing utils file",
479
- task: async () => {
480
- if (!shouldWriteUtils) return `Skipped — "${directories.utilsFilePath}" already exists`;
481
- const utilsContent = await fetchUtilsFileContent();
482
- await fsExtra.outputFile(utilsPath, utilsContent, "utf-8");
483
- return `${directories.utilsFilePath} written`;
484
- }
485
- },
486
475
  {
487
476
  title: "Preparing components directory",
488
477
  task: async () => {
@@ -585,7 +574,8 @@ async function checkIsComponentInstalled(componentName, config, cwd) {
585
574
  * 1. Resolve the full dependency tree for every requested component.
586
575
  * 2. Ask the user upfront whether to overwrite any that already exist.
587
576
  * 3. Fetch and write the source files for components the user approved.
588
- * 4. Install any npm packages required by the components being written.
577
+ * 4. Fetch and write the utils file if it doesn't exist but is required by the components.
578
+ * 5. Install any npm packages required by the components being written.
589
579
  */
590
580
  function add() {
591
581
  return defineCommand({
@@ -617,19 +607,32 @@ function add() {
617
607
  const packageDepsToInstall = {};
618
608
  for (const [name, entry] of allComponents) if (writeDecisions.get(name)) Object.assign(packageDepsToInstall, entry.packageDeps);
619
609
  const approvedComponents = Array.from(allComponents.entries()).filter(([name]) => writeDecisions.get(name));
620
- await tasks([...approvedComponents.map(([name, entry]) => ({
621
- title: `Installing ${name}`,
622
- task: async () => {
623
- for (const registryFilePath of entry.files) {
624
- const content = await fetchRegistryFileContent(registryFilePath);
625
- await writeFileWithDirs(mapRegistryPathToProjectDest(registryFilePath, config, cwd), content);
610
+ const needsUtils = Array.from(approvedComponents).some(([, entry]) => entry.needsUtils === true);
611
+ const utilsPath = join(cwd, config.utilsFilePath);
612
+ const utilsFileExists = await fsExtra.pathExists(utilsPath);
613
+ await tasks([
614
+ ...approvedComponents.map(([name, entry]) => ({
615
+ title: `Installing ${name}`,
616
+ task: async () => {
617
+ for (const registryFilePath of entry.files) {
618
+ const content = await fetchRegistryFileContent(registryFilePath);
619
+ await writeFileWithDirs(mapComponentsRegistryPathToProjectDest(registryFilePath, config, cwd), content);
620
+ }
621
+ return `${name} installed (${entry.files.length} file(s))`;
626
622
  }
627
- return `${name} installed (${entry.files.length} file(s))`;
628
- }
629
- })), {
630
- title: "Installing packages",
631
- task: async (message) => installMissingPackages(packageDepsToInstall, cwd, message)
632
- }]);
623
+ })),
624
+ {
625
+ title: "Installing packages",
626
+ task: async (message) => installMissingPackages(packageDepsToInstall, cwd, message)
627
+ },
628
+ ...needsUtils && !utilsFileExists ? [{
629
+ title: "Writing utils file",
630
+ task: async () => {
631
+ await writeFileWithDirs(utilsPath, await fetchUtilsFileContent());
632
+ return `${config.utilsFilePath} written`;
633
+ }
634
+ }] : []
635
+ ]);
633
636
  const skippedNames = Array.from(writeDecisions.entries()).filter(([, shouldWrite]) => !shouldWrite).map(([name]) => name);
634
637
  note([
635
638
  `Installed > ${approvedComponents.map(([name]) => name).join(", ") || "none"}`,
@@ -660,6 +663,7 @@ function add() {
660
663
  * know which of this component's packages become orphaned.
661
664
  * 4. Ask for confirmation, then delete the component directory and remove
662
665
  * any newly orphaned npm packages.
666
+ * 5. Check if any utils file is no longer needed and remove it if so(with confirmation from the user).
663
667
  */
664
668
  function remove() {
665
669
  return defineCommand({
@@ -689,14 +693,17 @@ function remove() {
689
693
  for (const name of remainingNames) {
690
694
  const entry = registry.components.find((component) => component.name === name);
691
695
  if (!entry) continue;
692
- if (entry.componentDeps.includes(componentName)) dependents.add(name);
693
- Object.assign(packagesStillNeeded, entry.packageDeps);
696
+ if (entry.componentDeps?.includes(componentName)) dependents.add(name);
697
+ Object.assign(packagesStillNeeded, entry.packageDeps || {});
694
698
  }
695
699
  if (dependents.size > 0) {
696
700
  const list = Array.from(dependents).map((dependent) => ` · ${dependent}`).join("\n");
697
701
  throw new Error(`Cannot remove "${componentName}" — the following installed components depend on it:\n${list}`);
698
702
  }
699
- const confirmed = await confirm({ message: `Remove "${componentName}"?` });
703
+ const confirmed = await confirm({
704
+ message: `Remove "${componentName}"?`,
705
+ initialValue: false
706
+ });
700
707
  if (isCancel(confirmed) || !confirmed) {
701
708
  cancel("Removal cancelled.");
702
709
  process.exit(0);
@@ -709,10 +716,25 @@ function remove() {
709
716
  }
710
717
  }, {
711
718
  title: "Removing orphaned packages",
712
- task: async (message) => removeOrphanedPackages(targetEntry.packageDeps, packagesStillNeeded, cwd, message)
719
+ task: async (message) => removeOrphanedPackages(targetEntry.packageDeps || {}, packagesStillNeeded, cwd, message)
713
720
  }]);
714
- const removedPackages = Object.keys(targetEntry.packageDeps).filter((pkg) => !(pkg in packagesStillNeeded));
721
+ const removedPackages = Object.keys(targetEntry.packageDeps || {}).filter((pkg) => !(pkg in packagesStillNeeded));
715
722
  if (removedPackages.length > 0) note(removedPackages.map((pkg) => ` · ${pkg}`).join("\n"), "Packages removed");
723
+ if (!remainingNames.some((name) => {
724
+ return registry.components.find((c) => c.name === name)?.needsUtils === true;
725
+ })) {
726
+ const utilsPath = join(cwd, config.utilsFilePath);
727
+ if (await fsExtra.pathExists(utilsPath)) {
728
+ const shouldDeleteUtils = await confirm({
729
+ message: "Delete utils file? (no components require it anymore)",
730
+ initialValue: false
731
+ });
732
+ if (!isCancel(shouldDeleteUtils) && shouldDeleteUtils) {
733
+ await fsExtra.remove(utilsPath);
734
+ note(`${config.utilsFilePath} deleted`, "Utils file removed");
735
+ }
736
+ }
737
+ }
716
738
  outro("All set!");
717
739
  } catch (error) {
718
740
  log.error(`Failed to remove component: ${error}`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "centoui-cli",
3
3
  "type": "module",
4
- "version": "1.0.0-alpha.36",
4
+ "version": "1.0.0-alpha.38",
5
5
  "private": false,
6
6
  "description": "Official CLI for CentoUI.",
7
7
  "keywords": [