centoui-cli 1.0.0-alpha.37 → 1.0.0-alpha.39

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/README.md CHANGED
@@ -89,23 +89,23 @@ This design eliminates an entire class of version mismatch bugs where the CLI an
89
89
  After running `centoui init`, your project root will contain a `centoui.config.ts`:
90
90
 
91
91
  ```ts
92
- import { defineConfig } from "centoui"
92
+ import { defineConfig } from 'centoui'
93
93
 
94
94
  export default defineConfig({
95
- componentsDir: "./src/components/centoui",
96
- themeFilePath: "./src/assets/css/centoui.css",
97
- utilsFilePath: "./src/utils/centoui-utils.ts",
95
+ componentsDir: './src/components/centoui',
98
96
  icons: {
99
- check: "lucide:check",
100
- close: "lucide:x",
101
- chevronDown: "lucide:chevron-down",
102
- chevronUp: "lucide:chevron-up",
103
- chevronLeft: "lucide:chevron-left",
104
- chevronRight: "lucide:chevron-right",
105
- chevronDoubleLeft: "lucide:chevrons-left",
106
- chevronDoubleRight: "lucide:chevrons-right",
107
- ellipsis: "lucide:ellipsis",
97
+ check: 'lucide:check',
98
+ chevronDoubleLeft: 'lucide:chevrons-left',
99
+ chevronDoubleRight: 'lucide:chevrons-right',
100
+ chevronDown: 'lucide:chevron-down',
101
+ chevronLeft: 'lucide:chevron-left',
102
+ chevronRight: 'lucide:chevron-right',
103
+ chevronUp: 'lucide:chevron-up',
104
+ close: 'lucide:x',
105
+ ellipsis: 'lucide:ellipsis',
108
106
  },
107
+ themeFilePath: './src/assets/css/centoui.css',
108
+ utilsFilePath: './src/utils/centoui-utils.ts',
109
109
  })
110
110
  ```
111
111
 
package/dist/index.d.mts CHANGED
@@ -1,63 +1,35 @@
1
1
  //#region src/types.d.ts
2
- /**
3
- * Mirrors the `globals.json` registry schema.
4
- * Defines properties that every CentoUI project must have.
5
- */
6
- type GlobalsRegistry = {
7
- /**
8
- * NPM packages that are required in every CentoUI project.
9
- * Keys are package names; values are semver version ranges (e.g. `"^4.2.2"`).
10
- */
11
- packageDeps: Record<string, string>;
12
- };
13
- /**
14
- * Mirrors the `component.json` registry schema.
15
- * Describes a single installable CentoUI component.
16
- */
17
- type ComponentRegistry = {
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;
20
- /**
21
- * Source file paths relative to `packages/core/src/`.
22
- * All paths start with `components/` by convention (e.g. `"components/button/button.vue"`).
23
- * The CLI fetches each file from GitHub and writes it into the user's components directory.
24
- */
25
- files: string[];
26
- /**
27
- * Names of other CentoUI components that must be installed alongside this one.
28
- * The CLI resolves the full dependency tree automatically.
29
- */
30
- componentDeps: string[];
31
- /**
32
- * NPM packages required specifically by this component,
33
- * in addition to the global dependencies defined in `GlobalsRegistry`.
34
- * Keys are package names; values are semver version ranges.
35
- */
36
- packageDeps: Record<string, string>;
37
- };
38
- /**
39
- * The complete CentoUI registry — the `index.json` file fetched from GitHub.
40
- * Contains global project dependencies and the full list of installable components.
41
- */
42
- type Registry = {
43
- /** Global properties that every CentoUI project must have. */globals: GlobalsRegistry; /** All components that can be installed with `centoui add`. */
44
- components: ComponentRegistry[];
45
- };
46
- /**
47
- * The user-side CentoUI configuration, stored in `centoui.config.ts`.
48
- * Generated by `centoui init` and consumed by every subsequent CLI command.
49
- */
50
- type CentoUIConfig = {
51
- /** Relative path (from project root) to the directory where components are installed. */componentsDir: string; /** Relative path (from project root) to the CSS file that receives theme and component styles. */
52
- themeFilePath: string; /** Relative path (from project root) to the utils file. */
53
- utilsFilePath: string;
54
- /**
55
- * Maps internal CentoUI icon slot names to Iconify icon IDs.
56
- * Components reference icon slots by their internal name so you can swap icon libraries freely.
57
- *
58
- * @example { menu: 'lucide:menu', close: 'lucide:x' }
59
- */
2
+ /** Describes a single installable CentoUI component. */
3
+ interface ComponentRegistryEntry {
4
+ /** Unique identifier for the component (e.g. `"button"`). */
5
+ name: string;
6
+ /** Source file paths relative to `packages/core/src/`. */
7
+ files: Array<string>;
8
+ /** Names of other CentoUI components required by this one. */
9
+ componentDependencies?: Array<string>;
10
+ /** NPM packages required by this component. */
11
+ npmDependencies?: Record<string, string>;
12
+ }
13
+ /** The complete CentoUI registry (`index.json`). */
14
+ interface Registry {
15
+ /** NPM packages required in every CentoUI project. */
16
+ npmDependencies: Record<string, string>;
17
+ /** Installable components. */
18
+ components: Array<ComponentRegistryEntry>;
19
+ }
20
+ /** User configuration stored in `centoui.config.ts`. */
21
+ interface CentoUIConfig {
22
+ /** Relative path to the component installation directory. */
23
+ componentsDir: string;
24
+ /** Relative path to the CSS file. */
25
+ themeFilePath: string;
26
+ /** Maps internal icon slot names to Iconify IDs. */
60
27
  icons: Record<string, string>;
61
- };
28
+ }
29
+ /** Partial package.json structure needed by the CLI. */
30
+ interface PackageJson {
31
+ dependencies?: Record<string, string>;
32
+ devDependencies?: Record<string, string>;
33
+ }
62
34
  //#endregion
63
- export { CentoUIConfig, ComponentRegistry, GlobalsRegistry, Registry };
35
+ export { CentoUIConfig, ComponentRegistryEntry, PackageJson, Registry };
package/dist/index.mjs CHANGED
@@ -1,14 +1,13 @@
1
- #!/usr/bin/env node
2
1
  import { defineCommand, runMain } from "citty";
3
- import { cancel, confirm, group, intro, isCancel, log, note, outro, tasks, text } from "@clack/prompts";
4
- import { dirname, join } from "pathe";
2
+ import { cancel, confirm, group, intro, isCancel, log, outro, tasks, text } from "@clack/prompts";
3
+ import { join } from "pathe";
4
+ import { loadConfig } from "c12";
5
5
  import fsExtra from "fs-extra";
6
6
  import { addDependency, removeDependency } from "nypm";
7
- import { loadConfig } from "c12";
8
7
  //#endregion
9
8
  //#region src/constants.ts
10
9
  /** CentoUI current package version, sourced directly from package.json. */
11
- const VERSION = "1.0.0-alpha.37";
10
+ const VERSION = "1.0.0-alpha.39";
12
11
  /** File name for the user-side CentoUI config (created by `centoui init`). */
13
12
  const CONFIG_FILE_NAME = "centoui.config.ts";
14
13
  /**
@@ -18,24 +17,7 @@ const CONFIG_FILE_NAME = "centoui.config.ts";
18
17
  * The path ends at `src/` so that registry and component paths from the
19
18
  * registry (e.g. `components/button/button.vue`) can be appended directly.
20
19
  */
21
- const CORE_SRC_BASE_URL = `https://raw.githubusercontent.com/favorodera/centoui/refs/tags/v${VERSION}/packages/core/src`;
22
- /** Full URL to the registry index file that lists every available component. */
23
- const REGISTRY_INDEX_URL = `${`${CORE_SRC_BASE_URL}/registry`}/index.json`;
24
- /**
25
- * Full URL to the CentoUI CSS theme file.
26
- * This file is written to the user's project during `centoui init`.
27
- */
28
- const THEME_CSS_URL = `${CORE_SRC_BASE_URL}/defaults/centoui.css`;
29
- /**
30
- * Full URL to the utils file.
31
- * This file is written to the user's project during `centoui init`.
32
- */
33
- const UTILS_FILE_URL = `${CORE_SRC_BASE_URL}/defaults/utils.ts`;
34
- /**
35
- * Full URL to the default values file for the CentoUI config.
36
- * The contents of this file are written to the user's project during `centoui init`.
37
- */
38
- const CONFIG_DEFAULTS_URL = `${CORE_SRC_BASE_URL}/defaults/config.ts`;
20
+ const BASE_URL = `https://raw.githubusercontent.com/favorodera/centoui/refs/tags/v${VERSION}/packages/core/src`;
39
21
  /**
40
22
  * HTTP headers required when fetching raw content from the GitHub API.
41
23
  * These ensure we get the raw file bytes, not GitHub's HTML wrapper.
@@ -45,679 +27,434 @@ const GITHUB_RAW_FETCH_HEADERS = {
45
27
  "X-GitHub-Api-Version": "2026-03-10"
46
28
  };
47
29
  //#endregion
48
- //#region src/utils/package-utils.ts
30
+ //#region src/utils/network.ts
49
31
  /**
50
- * Installs any packages from `requiredPackages` that are missing from or at
51
- * a different version than what is listed in the project's `package.json`.
52
- *
53
- * Already-satisfied packages are skipped entirely to avoid unnecessary network
54
- * traffic and lockfile churn. The package manager is auto-detected from
55
- * lockfiles by nypm (npm / pnpm / yarn / bun).
56
- *
57
- * @param requiredPackages - Map of `packageName → semver version range`.
58
- * @param cwd - Absolute path to the project root (must contain `package.json`).
59
- * @param onProgress - Optional callback fired before each `addDependency` call
60
- * with a `"[n/total] package@version"` string.
61
- * @returns Human-readable summary (e.g. `"Installed 3 package(s)"`).
62
- * @throws If reading `package.json` or running the package manager fails.
32
+ * Sends a network request to the CentoUI core package on GitHub.
33
+ * @param path The relative path to the file from the base URL (i.e. core/src).
34
+ * @param responseFormat The format of the response.
35
+ * @param init The request options.
36
+ * @returns The response from the server.
37
+ * @throws If the network request fails or the server returns a non-2xx status.
63
38
  */
64
- async function installMissingPackages(requiredPackages, cwd, onProgress) {
65
- if (Object.keys(requiredPackages).length === 0) return "No packages to install";
66
- const packageJson = await fsExtra.readJson(join(cwd, "package.json")).catch(() => ({}));
67
- const alreadyInstalled = {
68
- ...packageJson.dependencies,
69
- ...packageJson.devDependencies
39
+ async function sendNetworkRequest(path, responseFormat = "text", init) {
40
+ const url = `${BASE_URL}${path}`;
41
+ const resolvedInit = {
42
+ ...init,
43
+ headers: {
44
+ ...GITHUB_RAW_FETCH_HEADERS,
45
+ ...init?.headers
46
+ }
70
47
  };
71
- const packagesToInstall = Object.entries(requiredPackages).filter(([name, version]) => alreadyInstalled[name] !== version).map(([name, version]) => `${name}@${version}`);
72
- if (packagesToInstall.length === 0) return "All packages already up to date";
48
+ let response;
73
49
  try {
74
- for (const [index, pkg] of packagesToInstall.entries()) {
75
- onProgress?.(`[${index + 1}/${packagesToInstall.length}] ${pkg}`);
76
- await addDependency(pkg, {
77
- cwd,
78
- silent: true
79
- });
80
- }
50
+ response = await fetch(url, resolvedInit);
81
51
  } catch (error) {
82
- throw new Error(`[installMissingPackages] Failed to install packages: ${error}`);
52
+ throw new Error("Network request failed", { cause: error });
83
53
  }
84
- return `Installed ${packagesToInstall.length} package(s)`;
54
+ if (!response.ok) throw new Error(`Server responded with ${response.status} ${response.statusText} (URL: ${url})`);
55
+ let resolvedResponse;
56
+ switch (responseFormat) {
57
+ case "json":
58
+ resolvedResponse = await response.json();
59
+ break;
60
+ case "text":
61
+ resolvedResponse = await response.text();
62
+ break;
63
+ }
64
+ return resolvedResponse;
85
65
  }
66
+ //#endregion
67
+ //#region src/utils/config.ts
86
68
  /**
87
- * Removes packages that were used by a component being uninstalled, but only
88
- * if those packages are not still required by other remaining components.
89
- *
90
- * Skips any package that appears in `packagesStillNeeded` so that shared
91
- * dependencies are never removed prematurely.
92
- *
93
- * @param packagesToConsider - Packages belonging to the component being removed.
94
- * @param packagesStillNeeded - Union of `packageDeps` from all other installed components.
95
- * @param cwd - Absolute path to the project root.
96
- * @param onProgress - Optional callback fired before each `removeDependency` call.
97
- * @returns Human-readable summary (e.g. `"Removed 2 package(s)"`).
98
- * @throws If the package manager fails to remove a dependency.
69
+ * Loads the user's CentoUI configuration from `centoui.config.ts`.
70
+ * @param cwd Absolute path to the project root.
71
+ * @returns The user's configuration.
72
+ * @throws If `centoui.config.ts` is not found.
99
73
  */
100
- async function removeOrphanedPackages(packagesToConsider, packagesStillNeeded, cwd, onProgress) {
101
- const packagesToRemove = Object.keys(packagesToConsider).filter((name) => !(name in packagesStillNeeded));
102
- if (packagesToRemove.length === 0) return "No packages to remove";
103
- try {
104
- for (const [index, pkg] of packagesToRemove.entries()) {
105
- onProgress?.(`[${index + 1}/${packagesToRemove.length}] ${pkg}`);
106
- await removeDependency(pkg, {
107
- cwd,
108
- silent: true
109
- });
110
- }
111
- } catch (error) {
112
- throw new Error(`[removeOrphanedPackages] Failed to remove packages: ${error}`);
113
- }
114
- return `Removed ${packagesToRemove.length} package(s)`;
74
+ async function loadConfig$1(cwd) {
75
+ const { config, configFile } = await loadConfig({
76
+ cwd,
77
+ name: "centoui"
78
+ });
79
+ if (!configFile) throw new Error(`${CONFIG_FILE_NAME} not found in ${cwd}. Run \`centoui init\` first.`);
80
+ return config;
115
81
  }
116
82
  /**
117
- * Validates that a value is a non-empty string usable as a file-system path.
118
- *
119
- * Intended as a `validate` callback for `@clack/prompts` text inputs.
120
- *
121
- * @param value - The raw value from the prompt.
122
- * @returns An error message string if invalid, or `undefined` if valid.
83
+ * Builds the user's CentoUI configuration file content.
84
+ * @param choices The user's configuration choices.
85
+ * @returns The file content.
86
+ * @throws If building fails.
123
87
  */
124
- function validateNonEmptyPath(value) {
125
- if (typeof value !== "string") return "Expected a string path";
126
- if (value.trim().length === 0) return "Path cannot be empty";
88
+ async function buildUserConfig(choices) {
89
+ try {
90
+ let cleanedContent = (await sendNetworkRequest("/config.ts")).replaceAll(/^import\s+.*$/gm, "").replace(/^\s*export\s+default\s+/, "").replace(/\s+satisfies\s+[^}]*$/, "").trim();
91
+ const firstBrace = cleanedContent.indexOf("{");
92
+ const lastBrace = cleanedContent.lastIndexOf("}");
93
+ cleanedContent = cleanedContent.slice(firstBrace + 1, lastBrace).replace(/^\n/, "").replace(/\n\s*$/, "");
94
+ return `import { defineConfig } from 'centoui'
95
+
96
+ export default defineConfig({
97
+ componentsDir: '${choices.componentsDir}',
98
+ themeFilePath: '${choices.themeFilePath}',
99
+ ${cleanedContent}
100
+ })`;
101
+ } catch (error) {
102
+ throw new Error("Failed to build user config", { cause: error });
103
+ }
127
104
  }
128
105
  //#endregion
129
- //#region src/utils/file-system-utils.ts
106
+ //#region src/utils/file-system.ts
130
107
  /**
131
- * Converts a registry-relative file path into the absolute destination path
132
- * inside the user's project.
133
- *
134
- * Registry file paths always begin with `components/` (e.g.
135
- * `"components/button/button.vue"`). This function strips that leading segment
136
- * and joins the remainder with the user's configured components directory so
137
- * that `"components/button/button.vue"` becomes, for example,
138
- * `"/home/user/my-app/src/components/centoui/button/button.vue"`.
139
- *
140
- * @param registryFilePath - Path as it appears in the component's registry entry
141
- * (always starts with `"components/"`).
142
- * @param config - The loaded CentoUI project configuration.
143
- * @param cwd - Absolute path to the project root.
144
- * @returns Absolute destination path for the file in the user's project.
145
- *
146
- * @example
147
- * // config.componentsDir = 'src/components/centoui', cwd = '/home/user/my-app'
148
- * mapRegistryPathToProjectDest('components/button/button.vue', config, cwd)
149
- * // → '/home/user/my-app/src/components/centoui/button/button.vue'
108
+ * Writes content to a file, creating parent directories if they don't exist.
109
+ * @param path The file path to write to.
110
+ * @param content The content to write to the file.
111
+ * @throws If the file cannot be written to.
150
112
  */
151
- function mapRegistryPathToProjectDest(registryFilePath, config, cwd) {
152
- const pathWithoutRegistryPrefix = registryFilePath.replace(/^components\//, "");
153
- return join(cwd, config.componentsDir, pathWithoutRegistryPrefix);
113
+ async function writeToFile(path, content) {
114
+ try {
115
+ await fsExtra.outputFile(path, content, "utf8");
116
+ } catch (error) {
117
+ throw new Error(`Failed to write ${path}`, { cause: error });
118
+ }
154
119
  }
155
120
  /**
156
- * Writes `content` to `filePath`, creating every missing parent directory first.
157
- *
158
- * This is the standard way to write any file in the CLI — it ensures the
159
- * target directory tree exists so callers don't have to `mkdir` manually.
160
- *
161
- * @param filePath - Absolute path where the file should be written.
162
- * @param content - UTF-8 string content to write.
163
- * @throws If the directory cannot be created or the file cannot be written.
121
+ * Creates a directory and any necessary parent directories.
122
+ * @param path The directory path to create.
123
+ * @throws If the directory cannot be created.
164
124
  */
165
- async function writeFileWithDirs(filePath, content) {
125
+ async function createDirectory(path) {
166
126
  try {
167
- await fsExtra.mkdir(dirname(filePath), { recursive: true });
168
- await fsExtra.writeFile(filePath, content, "utf8");
127
+ await fsExtra.ensureDir(path);
169
128
  } catch (error) {
170
- throw new Error(`[writeFileWithDirs] Failed to write "${filePath}": ${error}`);
129
+ throw new Error(`Failed to create directory ${path}`, { cause: error });
171
130
  }
172
131
  }
173
132
  /**
174
133
  * Prompts the user to confirm before overwriting an existing path.
175
- *
176
- * If `path` does not yet exist, returns `true` immediately without showing
177
- * any prompt — nothing to overwrite, safe to proceed.
178
- *
179
- * If the user cancels the prompt (e.g. Ctrl+C), the process exits cleanly
180
- * with a cancellation message rather than throwing.
181
- *
182
- * @param label - Human-readable name shown in the prompt (e.g. `"centoui.config.ts"`).
183
- * @param path - Absolute path to the file or directory to potentially overwrite.
184
- * @returns `true` if the caller should write/overwrite, `false` if the user declined.
134
+ * @param path The absolute path to the file or directory to potentially overwrite.
135
+ * @returns Whether the user wants to overwrite the file.
185
136
  */
186
- async function confirmOverwriteIfExists(label, path) {
137
+ async function confirmOverwrite(path) {
187
138
  if (!await fsExtra.pathExists(path)) return true;
188
- const answer = await confirm({ message: `"${label}" already exists. Overwrite?` });
139
+ const answer = await confirm({
140
+ initialValue: false,
141
+ message: `${path} already exists. Overwrite?`
142
+ });
189
143
  if (isCancel(answer)) {
190
- cancel("Operation cancelled.");
144
+ cancel("Operation cancelled by user.");
191
145
  process.exit(0);
192
146
  }
193
147
  return answer;
194
148
  }
195
149
  //#endregion
196
- //#region src/utils/config-utils.ts
150
+ //#region src/utils/package.ts
197
151
  /**
198
- * Loads and returns the user's CentoUI configuration from `centoui.config.ts` using c12.
199
- *
200
- * @param cwd - Absolute path to the project root.
201
- * @returns The default export of `centoui.config.ts` cast as {@link CentoUIConfig}.
202
- * @throws If `centoui.config.ts` is not found — instructs the user to run `centoui init`.
203
- * @throws If the file exists but cannot be imported or does not export a default value.
204
- */
205
- async function loadCentoUIConfig(cwd) {
206
- const { config, configFile } = await loadConfig({
207
- name: "centoui",
208
- cwd
209
- });
210
- if (!configFile) throw new Error(`[loadCentoUIConfig] "${CONFIG_FILE_NAME}" not found in "${cwd}". Run \`centoui init\` first.`);
211
- return config;
212
- }
213
- /**
214
- * Fetches the raw content of the default CentoUI config file from GitHub.
215
- *
216
- * This file contains the default icon mappings and other shared defaults
217
- * that are merged into the user's generated config.
218
- *
219
- * @returns Raw UTF-8 content of the default config file.
220
- * @throws If the network request fails or the server returns a non-2xx status.
152
+ * Installs a dependency using nypm.
153
+ * @param name Dependency name.
154
+ * @param version Dependency semver version.
155
+ * @param cwd Current working directory.
156
+ * @returns Status message.
221
157
  */
222
- async function fetchDefaultConfig() {
223
- let response;
158
+ async function installDependency(name, version, cwd) {
224
159
  try {
225
- response = await fetch(CONFIG_DEFAULTS_URL, { headers: GITHUB_RAW_FETCH_HEADERS });
160
+ const packageJson = await fsExtra.readJson(join(cwd, "package.json")).catch(() => ({}));
161
+ if (name in {
162
+ ...packageJson?.dependencies,
163
+ ...packageJson?.devDependencies
164
+ }) return `${name} is already installed.`;
165
+ const target = `${name}@${version}`;
166
+ await addDependency(target, {
167
+ cwd,
168
+ silent: true
169
+ });
170
+ return `${target} installed.`;
226
171
  } catch (error) {
227
- throw new Error(`[fetchDefaultConfigContent] Network request for default config failed: ${error}`);
172
+ throw new Error(`Failed to install ${name}`, { cause: error });
228
173
  }
229
- if (!response.ok) throw new Error(`[fetchDefaultConfigContent] Server returned ${response.status} ${response.statusText} (URL: ${CONFIG_DEFAULTS_URL})`);
230
- return response.text();
231
174
  }
232
175
  /**
233
- * Extracts the inner config object from the default config file content.
234
- *
235
- * Strips the `export default`, `satisfies` clause, and outer braces,
236
- * returning just the inner properties as a raw string.
237
- *
238
- * @example
239
- * // Input:
240
- * // export default {
241
- * // icons: { check: 'lucide:check' },
242
- * // } satisfies Pick<CentoUIConfig, 'icons'>
243
- * // Output:
244
- * // " icons: { check: 'lucide:check' },"
245
- *
246
- * @param fileContent - The full source of the default config file.
247
- * @returns The inner config body, or an empty string if no object is found.
248
- */
249
- function extractInnerConfigContent(fileContent) {
250
- let cleanedConfigContent = fileContent.replace(/^import\s+.*$/gm, "");
251
- cleanedConfigContent = cleanedConfigContent.replace(/^\s*export\s+default\s+/, "");
252
- cleanedConfigContent = cleanedConfigContent.replace(/\s+satisfies\s+[^}]*$/, "");
253
- cleanedConfigContent = cleanedConfigContent.trim();
254
- const firstBrace = cleanedConfigContent.indexOf("{");
255
- const lastBrace = cleanedConfigContent.lastIndexOf("}");
256
- if (firstBrace === -1 || lastBrace === -1 || lastBrace <= firstBrace) return "";
257
- return cleanedConfigContent.slice(firstBrace + 1, lastBrace).replace(/^\n/, "").replace(/\n\s*$/, "");
258
- }
259
- /**
260
- * Generates the content of a default `centoui.config.ts` file.
261
- *
262
- * The generated file uses `defineConfig` so IDEs can provide type-checking
263
- * and autocompletion on the config object. It pre-fills common icon mappings
264
- * for the built-in Lucide icon set.
265
- *
266
- * @param themeFilePath - Relative path (from project root) where the CSS theme file lives.
267
- * @param componentsDir - Relative path (from project root) where components will be installed.
268
- * @returns A string containing the complete TypeScript source for the config file.
269
- */
270
- /**
271
- * Generates the content of a user's default `centoui.config.ts` file.
272
- *
273
- * Fetches the default config from GitHub to get the latest default config values (icon mappings, etc.),
274
- * then merges them with the user-provided paths. The generated file uses
275
- * `defineConfig` so IDEs can provide type-checking and autocompletion.
276
- *
277
- * @param themeFilePath - Relative path (from project root) where the CSS theme file lives.
278
- * @param componentsDir - Relative path (from project root) where components will be installed.
279
- * @param utilsFilePath - Relative path (from project root) where the utils file lives.
280
- * @returns A string containing the complete TypeScript source for the config file.
176
+ * Uninstalls a dependency using nypm.
177
+ * @param name Dependency name.
178
+ * @param cwd Current working directory.
179
+ * @returns Status message.
281
180
  */
282
- async function buildUserDefaultConfigFileContent(themeFilePath, componentsDir, utilsFilePath) {
283
- return `import { defineConfig } from 'centoui'
284
-
285
- export default defineConfig({
286
- componentsDir: '${componentsDir}',
287
- themeFilePath: '${themeFilePath}',
288
- utilsFilePath: '${utilsFilePath}',
289
- ${extractInnerConfigContent(await fetchDefaultConfig())}
290
- })
291
- `;
181
+ async function uninstallDependency(name, cwd) {
182
+ try {
183
+ const packageJson = await fsExtra.readJson(join(cwd, "package.json")).catch(() => ({}));
184
+ if (!(name in {
185
+ ...packageJson?.dependencies,
186
+ ...packageJson?.devDependencies
187
+ })) return `${name} is not installed.`;
188
+ await removeDependency(name, {
189
+ cwd,
190
+ silent: true
191
+ });
192
+ return `${name} uninstalled.`;
193
+ } catch (error) {
194
+ throw new Error(`Failed to uninstall ${name}`, { cause: error });
195
+ }
292
196
  }
293
197
  //#endregion
294
- //#region src/utils/registry-utils.ts
198
+ //#region src/utils/registry.ts
295
199
  /** In-process cache so the registry is only fetched once per CLI invocation. */
296
- let cachedRegistry = null;
200
+ let cachedRegistry;
297
201
  /**
298
- * Fetches the complete component registry (`index.json`) from GitHub, caching
299
- * the result in memory for the lifetime of the process.
300
- *
301
- * Every other registry utility calls this internally, so the network round-trip
302
- * only happens once regardless of how many components are being installed.
303
- *
304
- * @returns The full registry object including globals and all component entries.
305
- * @throws If the network request fails or the server returns a non-2xx status.
202
+ * Fetches the component registry (`index.json`) from GitHub and caches it.
203
+ * @returns The registry including npm dependencies and components.
306
204
  */
307
- async function fetchFullRegistry() {
205
+ async function fetchRegistry() {
308
206
  if (cachedRegistry) return cachedRegistry;
309
- let response;
310
- try {
311
- response = await fetch(REGISTRY_INDEX_URL, { headers: GITHUB_RAW_FETCH_HEADERS });
312
- } catch (error) {
313
- throw new Error(`[fetchFullRegistry] Network request to registry failed: ${error}`);
314
- }
315
- if (!response.ok) throw new Error(`[fetchFullRegistry] Registry responded with ${response.status} ${response.statusText} (URL: ${REGISTRY_INDEX_URL})`);
316
- cachedRegistry = await response.json();
317
- return cachedRegistry;
207
+ const registry = await sendNetworkRequest("/registry/index.json", "json");
208
+ cachedRegistry = registry;
209
+ return registry;
318
210
  }
319
211
  /**
320
- * Recursively resolves a component and every one of its transitive
321
- * `componentDeps` into a flat map of `componentName → registry entry`.
322
- *
323
- * The traversal is depth-first and guards against circular dependencies via
324
- * the `visited` set, so each component appears in the result exactly once.
325
- *
326
- * @param componentName - Root component name to start resolving from.
327
- * @param registry - Pre-fetched registry (pass the result of {@link fetchFullRegistry}).
328
- * @param visited - Internal visited set used to prevent infinite loops;
329
- * callers should omit this — it defaults to an empty set.
330
- * @returns A `Map<componentName, ComponentRegistry>` covering the full tree.
331
- * @throws If any component in the tree is not found in the registry.
212
+ * Recursively resolves a component and its dependencies into a map.
213
+ * @param name Component name.
214
+ * @param registry The component registry.
215
+ * @param result Internal accumulator map.
216
+ * @returns Map of resolved component names to their entries.
217
+ * @throws If any component is not found.
332
218
  */
333
- function resolveComponentWithDependencies(componentName, registry, visited = /* @__PURE__ */ new Set()) {
334
- const result = /* @__PURE__ */ new Map();
335
- if (visited.has(componentName)) return result;
336
- visited.add(componentName);
337
- const entry = registry.components.find((component) => component.name === componentName);
338
- if (!entry) throw new Error(`[resolveComponentWithDependencies] Component "${componentName}" not found in registry.`);
339
- result.set(componentName, entry);
340
- for (const dep of entry.componentDeps) for (const [depName, depEntry] of resolveComponentWithDependencies(dep, registry, visited)) result.set(depName, depEntry);
219
+ function resolveComponent(name, registry, result = /* @__PURE__ */ new Map()) {
220
+ if (result.has(name)) return result;
221
+ const entry = registry.components.find((component) => component.name === name);
222
+ if (!entry) throw new Error(`Component ${name} not found in registry.`);
223
+ result.set(name, entry);
224
+ for (const component of entry.componentDependencies || []) resolveComponent(component, registry, result);
341
225
  return result;
342
226
  }
227
+ //#endregion
228
+ //#region src/commands/add.ts
343
229
  /**
344
- * Fetches the raw source content of a file referenced in a component's
345
- * registry entry.
346
- *
347
- * Registry file paths (e.g. `"components/button/button.vue"`) are relative
348
- * to `packages/core/src/`, which is already the base of {@link CORE_SRC_BASE_URL}.
349
- * This function simply appends the path and downloads the content.
350
- *
351
- * @param registryFilePath - Path as it appears in the component's `files` array
352
- * (e.g. `"components/button/button.vue"`).
353
- * @returns Raw UTF-8 content of the file.
354
- * @throws If the network request fails or the server returns a non-2xx status.
355
- */
356
- async function fetchRegistryFileContent(registryFilePath) {
357
- const url = `${CORE_SRC_BASE_URL}/${registryFilePath}`;
358
- let response;
359
- try {
360
- response = await fetch(url, { headers: GITHUB_RAW_FETCH_HEADERS });
361
- } catch (error) {
362
- throw new Error(`[fetchRegistryFileContent] Network request failed for "${registryFilePath}": ${error}`);
363
- }
364
- if (!response.ok) throw new Error(`[fetchRegistryFileContent] Server returned ${response.status} ${response.statusText} for "${url}"`);
365
- return response.text();
366
- }
367
- /**
368
- * Fetches the raw content of the CentoUI CSS theme file from GitHub.
369
- *
370
- * This is the file written to the user's project during `centoui init` and
371
- * contains all CSS custom properties and base styles for every component.
372
- *
373
- * @returns Raw UTF-8 content of the theme CSS file.
374
- * @throws If the network request fails or the server returns a non-2xx status.
375
- */
376
- async function fetchThemeCSSContent() {
377
- let response;
378
- try {
379
- response = await fetch(THEME_CSS_URL, { headers: GITHUB_RAW_FETCH_HEADERS });
380
- } catch (error) {
381
- throw new Error(`[fetchThemeCSSContent] Network request for theme CSS failed: ${error}`);
382
- }
383
- if (!response.ok) throw new Error(`[fetchThemeCSSContent] Server returned ${response.status} ${response.statusText} (URL: ${THEME_CSS_URL})`);
384
- return response.text();
385
- }
386
- /**
387
- * Fetches the raw content of the CentoUI utils file from GitHub.
388
- *
389
- * This is the file written to the user's project during `centoui init` and
390
- * contains all global utils for every component.
391
- *
392
- * @returns Raw UTF-8 content of the utils file.
393
- * @throws If the network request fails or the server returns a non-2xx status.
230
+ * Installs one or more components and their full transitive dependency trees from the registry into the user's project.
231
+ * @returns The Citty command definition that executes the 'add' CLI process.
394
232
  */
395
- async function fetchUtilsFileContent() {
396
- let response;
397
- try {
398
- response = await fetch(UTILS_FILE_URL, { headers: GITHUB_RAW_FETCH_HEADERS });
399
- } catch (error) {
400
- throw new Error(`[fetchUtilsFileContent] Network request for utils file failed: ${error}`);
401
- }
402
- if (!response.ok) throw new Error(`[fetchUtilsFileContent] Server returned ${response.status} ${response.statusText} (URL: ${UTILS_FILE_URL})`);
403
- return response.text();
233
+ async function add() {
234
+ return defineCommand({
235
+ meta: {
236
+ description: "Add one or more components to your project",
237
+ name: "add"
238
+ },
239
+ args: {},
240
+ run: async ({ args }) => {
241
+ const cwd = process.cwd();
242
+ intro("CentoUI — Add components!");
243
+ const requestedComponents = args._;
244
+ if (requestedComponents.length === 0) throw new Error("No components specified. Usage: centoui add <component> [component...]");
245
+ log.step("Loading config.");
246
+ const config = await loadConfig$1(cwd);
247
+ log.step("Fetching registry.");
248
+ const registry = await fetchRegistry();
249
+ log.step("Resolving components");
250
+ const resolvedComponents = /* @__PURE__ */ new Map();
251
+ for (const requested of requestedComponents) {
252
+ const resolvedTree = resolveComponent(requested, registry);
253
+ for (const [name, entry] of resolvedTree) resolvedComponents.set(name, entry);
254
+ }
255
+ const writeDecisions = /* @__PURE__ */ new Map();
256
+ for (const [name] of resolvedComponents) {
257
+ const shouldWrite = await confirmOverwrite(join(cwd, config.componentsDir, name));
258
+ writeDecisions.set(name, shouldWrite);
259
+ }
260
+ const npmDependenciesToInstall = {};
261
+ for (const [name, entry] of resolvedComponents) if (writeDecisions.get(name)) Object.assign(npmDependenciesToInstall, entry.npmDependencies);
262
+ await tasks([...[...resolvedComponents.entries()].filter(([name]) => writeDecisions.get(name)).map(([name, entry]) => {
263
+ return {
264
+ task: async (message) => {
265
+ message(`Installing ${name}.`);
266
+ for (const path of entry.files) {
267
+ message(`Fetching contents from registry.`);
268
+ const content = await sendNetworkRequest(`/components/${path}`);
269
+ const destination = join(cwd, config.componentsDir, path);
270
+ message("Writing to disk.");
271
+ await writeToFile(destination, content);
272
+ }
273
+ return `${name} component installed!`;
274
+ },
275
+ title: `Installing ${name}`
276
+ };
277
+ }), {
278
+ enabled: Object.keys(npmDependenciesToInstall).length > 0,
279
+ task: async (message) => {
280
+ for (const [name, version] of Object.entries(npmDependenciesToInstall)) {
281
+ message(`Installing ${name}.`);
282
+ await installDependency(name, version, cwd);
283
+ }
284
+ return "Dependencies installed!";
285
+ },
286
+ title: "Installing dependencies"
287
+ }]);
288
+ outro("Installation Complete!");
289
+ }
290
+ });
404
291
  }
405
292
  //#endregion
406
293
  //#region src/commands/init.ts
407
294
  /**
408
- * Command: `centoui init`
409
- *
410
295
  * Bootstraps a new CentoUI project in the current working directory.
411
- *
412
- * 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
- * 3. Write the config file, fetch and write the theme CSS, prepare the
417
- * components directory, and install global npm dependencies.
296
+ * @returns The Citty command definition that executes the 'init' CLI process.
418
297
  */
419
298
  function init() {
420
299
  return defineCommand({
421
300
  meta: {
422
- name: "init",
423
- description: "Initialize a new CentoUI project"
301
+ description: "Initialize a new CentoUI project",
302
+ name: "init"
424
303
  },
425
- async run() {
426
- try {
427
- const cwd = process.cwd();
428
- intro("CentoUI Initialize project");
429
- const directories = await group({
430
- componentDir: () => text({
431
- message: "Directory to store components",
432
- initialValue: "src/components/centoui",
433
- validate: validateNonEmptyPath
434
- }),
435
- themeFilePath: () => text({
436
- message: "Path for the theme CSS file",
437
- initialValue: "src/assets/css/centoui.css",
438
- validate: validateNonEmptyPath
439
- }),
440
- utilsFilePath: () => text({
441
- message: "Path for the utils file",
442
- initialValue: "src/utils/centoui-utils.ts",
443
- validate: validateNonEmptyPath
444
- })
445
- }, { onCancel: () => {
446
- cancel("Initialization cancelled.");
447
- process.exit(0);
448
- } });
449
- const configPath = join(cwd, CONFIG_FILE_NAME);
450
- const themePath = join(cwd, directories.themeFilePath);
451
- const componentsPath = join(cwd, directories.componentDir);
452
- const utilsPath = join(cwd, directories.utilsFilePath);
453
- const shouldWriteConfig = await confirmOverwriteIfExists(CONFIG_FILE_NAME, configPath);
454
- const shouldWriteTheme = await confirmOverwriteIfExists(directories.themeFilePath, themePath);
455
- const shouldWriteComponentsDir = await confirmOverwriteIfExists(directories.componentDir, componentsPath);
456
- const shouldWriteUtils = await confirmOverwriteIfExists(directories.utilsFilePath, utilsPath);
457
- let registry;
458
- await tasks([
459
- {
460
- title: "Fetching config defaults",
461
- task: async () => {
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");
465
- return `${CONFIG_FILE_NAME} written`;
466
- }
304
+ run: async () => {
305
+ const cwd = process.cwd();
306
+ intro("CentoUI Initialize project!");
307
+ const choices = await group({
308
+ componentsDir: () => text({
309
+ initialValue: "src/components/centoui",
310
+ message: "Directory to store components."
311
+ }),
312
+ themeFilePath: () => text({
313
+ initialValue: "src/assets/css/centoui.css",
314
+ message: "Path for the theme CSS file."
315
+ })
316
+ }, { onCancel: () => {
317
+ cancel("Project initialization cancelled.");
318
+ process.exit(0);
319
+ } });
320
+ const configPath = join(cwd, CONFIG_FILE_NAME);
321
+ const themePath = join(cwd, choices.themeFilePath);
322
+ const componentsPath = join(cwd, choices.componentsDir);
323
+ const shouldWriteConfig = await confirmOverwrite(CONFIG_FILE_NAME);
324
+ const shouldWriteTheme = await confirmOverwrite(choices.themeFilePath);
325
+ const shouldWriteComponentsDir = await confirmOverwrite(choices.componentsDir);
326
+ let registry;
327
+ await tasks([
328
+ {
329
+ enabled: shouldWriteConfig,
330
+ task: async (message) => {
331
+ message("Building config content.");
332
+ const userConfigContent = await buildUserConfig(choices);
333
+ message("Writing to disk.");
334
+ await writeToFile(configPath, userConfigContent);
335
+ return "Config created!";
467
336
  },
468
- {
469
- title: "Fetching theme CSS",
470
- task: async () => {
471
- if (!shouldWriteTheme) return `Skipped — "${directories.themeFilePath}" already exists`;
472
- const themeContent = await fetchThemeCSSContent();
473
- await fsExtra.outputFile(themePath, themeContent, "utf-8");
474
- return `${directories.themeFilePath} written`;
475
- }
337
+ title: "Creating config."
338
+ },
339
+ {
340
+ enabled: shouldWriteTheme,
341
+ task: async (message) => {
342
+ message("Fetching theme from registry.");
343
+ const themeFileContent = await sendNetworkRequest("/theme.css");
344
+ message("Writing to disk.");
345
+ await writeToFile(themePath, themeFileContent);
346
+ return "Theme created!";
476
347
  },
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
- }
348
+ title: "Creating theme."
349
+ },
350
+ {
351
+ enabled: shouldWriteComponentsDir,
352
+ task: async (message) => {
353
+ message("Writing to disk.");
354
+ await createDirectory(componentsPath);
355
+ return "Components directory ready!";
485
356
  },
486
- {
487
- title: "Preparing components directory",
488
- task: async () => {
489
- if (!shouldWriteComponentsDir) return `Skipped — "${directories.componentDir}" already exists`;
490
- await fsExtra.emptyDir(componentsPath);
491
- return `${directories.componentDir} ready`;
492
- }
357
+ title: "Preparing components directory."
358
+ },
359
+ {
360
+ task: async () => {
361
+ registry = await fetchRegistry();
362
+ return "Registry loaded!";
493
363
  },
494
- {
495
- title: "Fetching registry",
496
- task: async () => {
497
- registry = await fetchFullRegistry();
498
- return "Registry loaded";
364
+ title: "Loading registry."
365
+ },
366
+ {
367
+ task: async (message) => {
368
+ for (const [name, version] of Object.entries(registry.npmDependencies)) {
369
+ message(`Installing ${name}.`);
370
+ await installDependency(name, version, cwd);
499
371
  }
372
+ return "Dependencies installed!";
500
373
  },
501
- {
502
- title: "Installing global dependencies",
503
- task: async (message) => installMissingPackages(registry.globals.packageDeps, cwd, message)
504
- }
505
- ]);
506
- note([
507
- `Config > ${configPath}`,
508
- `Theme > ${themePath}`,
509
- `Components > ${componentsPath}`,
510
- "",
511
- "Run 'centoui add button' to install your first component."
512
- ].filter(Boolean).join("\n"), "CentoUI initialized");
513
- outro("All set!");
514
- } catch (error) {
515
- log.error(`Initialization failed: ${error}`);
516
- process.exit(1);
517
- }
374
+ title: "Installing dependencies."
375
+ }
376
+ ]);
377
+ outro("Initialization Complete!");
518
378
  }
519
379
  });
520
380
  }
521
381
  //#endregion
522
- //#region src/utils/components-utils.ts
382
+ //#region src/utils/components.ts
523
383
  /**
524
- * Scans the configured components directory and returns a sorted list of
525
- * installed component names.
526
- *
527
- * Each direct subdirectory of `config.componentsDir` is treated as one
528
- * installed component. Non-directory entries (loose files) are ignored.
529
- *
530
- * @param config - The loaded CentoUI project configuration.
531
- * @param cwd - Absolute path to the project root.
532
- * @returns Sorted array of installed component names, or an empty array if
533
- * the components directory does not exist yet.
534
- * @throws If the directory exists but cannot be read.
535
- */
536
- async function listInstalledComponentNames(config, cwd) {
537
- const componentsDir = join(cwd, config.componentsDir);
538
- try {
539
- if (!await fsExtra.pathExists(componentsDir)) return [];
540
- return (await fsExtra.readdir(componentsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
541
- } catch (error) {
542
- throw new Error(`[listInstalledComponentNames] Failed to read components directory "${componentsDir}": ${error}`);
543
- }
544
- }
545
- /**
546
- * Returns the absolute path to a component's installation directory inside
547
- * the user's project.
548
- *
549
- * The directory may or may not exist yet — this function does not check.
550
- * Use {@link checkIsComponentInstalled} if you need to verify existence.
551
- *
552
- * @param componentName - The component name in kebab-case (e.g. `"button"`).
553
- * @param config - The loaded CentoUI project configuration.
554
- * @param cwd - Absolute path to the project root.
555
- */
556
- function resolveComponentInstallDir(componentName, config, cwd) {
557
- return join(cwd, config.componentsDir, componentName);
558
- }
559
- /**
560
- * Checks whether a component is currently installed by testing for the
561
- * existence of its installation directory.
562
- *
563
- * @param componentName - The component name in kebab-case (e.g. `"button"`).
564
- * @param config - The loaded CentoUI project configuration.
565
- * @param cwd - Absolute path to the project root.
566
- * @returns `true` if the component directory exists, `false` otherwise.
384
+ * Scans the components directory to determine which CentoUI components are installed.
385
+ * @param config User's CentoUI configuration.
386
+ * @param cwd Absolute path to the project root.
387
+ * @returns Sorted array of installed component names.
388
+ * @throws If the components directory cannot be read.
567
389
  */
568
- async function checkIsComponentInstalled(componentName, config, cwd) {
569
- const componentInstallDir = resolveComponentInstallDir(componentName, config, cwd);
390
+ async function getInstalledComponents(config, cwd) {
570
391
  try {
571
- return fsExtra.pathExists(componentInstallDir);
392
+ const componentsDir = join(cwd, config.componentsDir);
393
+ return (await fsExtra.readdir(componentsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).toSorted();
572
394
  } catch (error) {
573
- throw new Error(`[checkIsComponentInstalled] Failed to check if component "${componentName}" is installed: ${error}`);
395
+ throw new Error("Failed to read components directory", { cause: error });
574
396
  }
575
397
  }
576
398
  //#endregion
577
- //#region src/commands/add.ts
578
- /**
579
- * Command: `centoui add <component> [component...]`
580
- *
581
- * Installs one or more components — and their full transitive dependency trees
582
- * — from the registry into the user's project.
583
- *
584
- * Flow:
585
- * 1. Resolve the full dependency tree for every requested component.
586
- * 2. Ask the user upfront whether to overwrite any that already exist.
587
- * 3. Fetch and write the source files for components the user approved.
588
- * 4. Install any npm packages required by the components being written.
589
- */
590
- function add() {
591
- return defineCommand({
592
- meta: {
593
- name: "add",
594
- description: "Add one or more components to your project"
595
- },
596
- args: {},
597
- async run({ args }) {
598
- try {
599
- const cwd = process.cwd();
600
- const requestedNames = args._;
601
- intro("CentoUI — Add components");
602
- if (requestedNames.length === 0) throw new Error("No components specified. Usage: centoui add <component> [component...]");
603
- const config = await loadCentoUIConfig(cwd);
604
- const registry = await fetchFullRegistry();
605
- const allComponents = /* @__PURE__ */ new Map();
606
- for (const name of requestedNames) try {
607
- const tree = resolveComponentWithDependencies(name, registry);
608
- for (const [depName, depEntry] of tree) allComponents.set(depName, depEntry);
609
- } catch (error) {
610
- throw new Error(`Failed to resolve "${name}": ${error}`);
611
- }
612
- const writeDecisions = /* @__PURE__ */ new Map();
613
- for (const [name] of allComponents) {
614
- const shouldWrite = await confirmOverwriteIfExists(name, resolveComponentInstallDir(name, config, cwd));
615
- writeDecisions.set(name, shouldWrite);
616
- }
617
- const packageDepsToInstall = {};
618
- for (const [name, entry] of allComponents) if (writeDecisions.get(name)) Object.assign(packageDepsToInstall, entry.packageDeps);
619
- 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);
626
- }
627
- return `${name} installed (${entry.files.length} file(s))`;
628
- }
629
- })), {
630
- title: "Installing packages",
631
- task: async (message) => installMissingPackages(packageDepsToInstall, cwd, message)
632
- }]);
633
- const skippedNames = Array.from(writeDecisions.entries()).filter(([, shouldWrite]) => !shouldWrite).map(([name]) => name);
634
- note([
635
- `Installed > ${approvedComponents.map(([name]) => name).join(", ") || "none"}`,
636
- skippedNames.length > 0 ? `Skipped > ${skippedNames.join(", ")}` : "",
637
- "",
638
- "Import components from your components directory to use them."
639
- ].filter(Boolean).join("\n"), "Component(s) added");
640
- outro("All set!");
641
- } catch (error) {
642
- log.error(`Failed to add component(s): ${error}`);
643
- process.exit(1);
644
- }
645
- }
646
- });
647
- }
648
- //#endregion
649
399
  //#region src/commands/remove.ts
650
400
  /**
651
- * Command: `centoui remove <component>`
652
- *
653
401
  * Removes a single installed component and its files from the user's project.
654
- *
655
- * Flow:
656
- * 1. Confirm the component is actually installed.
657
- * 2. Fetch the registry and check whether any other installed components
658
- * declare this one as a `componentDep` — block removal if so.
659
- * 3. Collect the packages still required by remaining components so we
660
- * know which of this component's packages become orphaned.
661
- * 4. Ask for confirmation, then delete the component directory and remove
662
- * any newly orphaned npm packages.
402
+ * @returns The Citty command definition that executes the 'remove' CLI process.
663
403
  */
664
404
  function remove() {
665
405
  return defineCommand({
666
406
  meta: {
667
- name: "remove",
668
- description: "Remove an installed component from your project"
407
+ description: "Remove an installed component from your project",
408
+ name: "remove"
669
409
  },
670
410
  args: { component: {
671
- type: "positional",
411
+ description: "Name of the component to remove (e.g. \"button\")",
672
412
  required: true,
673
- description: "Name of the component to remove (e.g. \"button\")"
413
+ type: "positional"
674
414
  } },
675
- async run({ args }) {
676
- try {
677
- const cwd = process.cwd();
678
- const componentName = args.component;
679
- intro("CentoUI — Remove component");
680
- const config = await loadCentoUIConfig(cwd);
681
- if (!await checkIsComponentInstalled(componentName, config, cwd)) throw new Error(`Component "${componentName}" is not installed.`);
682
- const componentInstallDir = resolveComponentInstallDir(componentName, config, cwd);
683
- const registry = await fetchFullRegistry();
684
- const targetEntry = registry.components.find((component) => component.name === componentName);
685
- if (!targetEntry) throw new Error(`"${componentName}" was not found in the registry. Aborting to avoid a partial removal.`);
686
- const remainingNames = (await listInstalledComponentNames(config, cwd)).filter((name) => name !== componentName);
687
- const dependents = /* @__PURE__ */ new Set();
688
- const packagesStillNeeded = {};
689
- for (const name of remainingNames) {
690
- const entry = registry.components.find((component) => component.name === name);
691
- if (!entry) continue;
692
- if (entry.componentDeps.includes(componentName)) dependents.add(name);
693
- Object.assign(packagesStillNeeded, entry.packageDeps);
694
- }
695
- if (dependents.size > 0) {
696
- const list = Array.from(dependents).map((dependent) => ` · ${dependent}`).join("\n");
697
- throw new Error(`Cannot remove "${componentName}" — the following installed components depend on it:\n${list}`);
698
- }
699
- const confirmed = await confirm({ message: `Remove "${componentName}"?` });
700
- if (isCancel(confirmed) || !confirmed) {
701
- cancel("Removal cancelled.");
702
- process.exit(0);
703
- }
704
- await tasks([{
705
- title: `Removing ${componentName}`,
706
- task: async () => {
707
- await fsExtra.remove(componentInstallDir);
708
- return `${componentName} removed`;
709
- }
710
- }, {
711
- title: "Removing orphaned packages",
712
- task: async (message) => removeOrphanedPackages(targetEntry.packageDeps, packagesStillNeeded, cwd, message)
713
- }]);
714
- const removedPackages = Object.keys(targetEntry.packageDeps).filter((pkg) => !(pkg in packagesStillNeeded));
715
- if (removedPackages.length > 0) note(removedPackages.map((pkg) => ` · ${pkg}`).join("\n"), "Packages removed");
716
- outro("All set!");
717
- } catch (error) {
718
- log.error(`Failed to remove component: ${error}`);
719
- process.exit(1);
415
+ run: async ({ args }) => {
416
+ const cwd = process.cwd();
417
+ const component = args.component;
418
+ intro("CentoUI Remove components!");
419
+ log.step("Loading config.");
420
+ const config = await loadConfig$1(cwd);
421
+ log.step("Resolving workspace.");
422
+ const installedComponents = await getInstalledComponents(config, cwd);
423
+ if (!installedComponents.includes(component)) throw new Error(`${component} is not installed.`);
424
+ log.step("Fetching registry.");
425
+ const registry = await fetchRegistry();
426
+ log.step("Resolving components.");
427
+ const componentEntry = registry.components.find((entry) => entry.name === component);
428
+ if (!componentEntry) throw new Error(`${component} not found in registry.`);
429
+ const exceptToBeRemovedComponents = installedComponents.filter((entry) => entry !== component);
430
+ const componentDependents = /* @__PURE__ */ new Set();
431
+ const neededDependencies = /* @__PURE__ */ new Map();
432
+ for (const name of exceptToBeRemovedComponents) {
433
+ const entry = registry.components.find((entry) => entry.name === name);
434
+ if (entry?.componentDependencies?.includes(component)) componentDependents.add(name);
435
+ neededDependencies.set(name, entry?.npmDependencies || {});
720
436
  }
437
+ if (componentDependents.size > 0) throw new Error(`Cannot remove ${component} these components depend on it: ${[...componentDependents].join(", ")}`);
438
+ const componentDir = join(cwd, config.componentsDir, component);
439
+ const dependenciesToUninstall = Object.keys(componentEntry?.npmDependencies || {}).filter((name) => !neededDependencies.has(name));
440
+ await tasks([{
441
+ task: async () => {
442
+ await fsExtra.remove(componentDir);
443
+ return `${component} removed!`;
444
+ },
445
+ title: `Removing ${component}.`
446
+ }, {
447
+ enabled: dependenciesToUninstall.length > 0,
448
+ task: async (message) => {
449
+ for (const name of dependenciesToUninstall) {
450
+ message(`Uninstalling ${name}.`);
451
+ await uninstallDependency(name, cwd);
452
+ }
453
+ return "Orphaned dependencies removed!";
454
+ },
455
+ title: `Removing orphaned dependencies`
456
+ }]);
457
+ outro(`${component} removed from your project!`);
721
458
  }
722
459
  });
723
460
  }
@@ -729,8 +466,8 @@ runMain(defineCommand({
729
466
  version: VERSION
730
467
  },
731
468
  subCommands: {
732
- init,
733
469
  add,
470
+ init,
734
471
  remove
735
472
  }
736
473
  }));
package/package.json CHANGED
@@ -1,18 +1,12 @@
1
1
  {
2
2
  "name": "centoui-cli",
3
3
  "type": "module",
4
- "version": "1.0.0-alpha.37",
4
+ "version": "1.0.0-alpha.39",
5
5
  "private": false,
6
6
  "description": "Official CLI for CentoUI.",
7
- "keywords": [
8
- "cli",
9
- "vue",
10
- "ui",
11
- "components",
12
- "tailwindcss"
13
- ],
14
7
  "author": "Favour Emeka <favorodera@gmail.com>",
15
8
  "license": "MIT",
9
+ "funding": "https://github.com/sponsors/favorodera",
16
10
  "homepage": "https://centoui.vercel.app",
17
11
  "repository": {
18
12
  "type": "git",
@@ -21,6 +15,13 @@
21
15
  "bugs": {
22
16
  "url": "https://github.com/favorodera/centoui/issues"
23
17
  },
18
+ "keywords": [
19
+ "cli",
20
+ "vue",
21
+ "ui",
22
+ "components",
23
+ "tailwindcss"
24
+ ],
24
25
  "publishConfig": {
25
26
  "access": "public"
26
27
  },
@@ -29,35 +30,34 @@
29
30
  "./package.json": "./package.json"
30
31
  },
31
32
  "types": "./dist/types.d.mts",
32
- "files": [
33
- "dist"
34
- ],
35
33
  "bin": {
36
34
  "centoui": "./dist/index.mjs"
37
35
  },
38
- "devDependencies": {
39
- "@types/fs-extra": "^11.0.4",
40
- "@vitest/ui": "^4.1.5",
41
- "tsdown": "^0.21.7",
42
- "type-fest": "^5.6.0",
43
- "vitest": "^4.1.2"
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "engines": {
40
+ "node": ">=22.0.0",
41
+ "pnpm": ">=11.0.0"
44
42
  },
45
43
  "dependencies": {
46
- "@clack/prompts": "^1.2.0",
47
- "c12": "4.0.0-beta.5",
44
+ "@clack/prompts": "^1.5.1",
45
+ "c12": "^4.0.0-beta.5",
48
46
  "citty": "^0.2.2",
49
- "fs-extra": "^11.3.4",
50
- "nypm": "^0.6.6",
47
+ "fs-extra": "^11.3.5",
48
+ "nypm": "^0.6.7",
51
49
  "pathe": "^2.0.3"
52
50
  },
53
- "engines": {
54
- "node": ">=22.0.0",
55
- "pnpm": ">=11.0.0"
51
+ "devDependencies": {
52
+ "@types/fs-extra": "^11.0.4",
53
+ "@vitest/ui": "^4.1.8",
54
+ "tsdown": "^0.22.2",
55
+ "vitest": "^4.1.8"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsdown",
59
59
  "dev": "tsdown --watch",
60
- "test": "vitest",
60
+ "test": "vitest run",
61
61
  "test:watch": "vitest watch --ui",
62
62
  "typecheck": "tsc --noEmit",
63
63
  "lint": "eslint . --fix"