centoui-cli 0.2.0 → 0.2.2

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.
Files changed (3) hide show
  1. package/LICENSE +21 -21
  2. package/dist/index.mjs +166 -3
  3. package/package.json +1 -1
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Favour Emeka
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Favour Emeka
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.mjs CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { defineCommand, runMain } from "citty";
3
3
  import { cancel, confirm, group, intro, isCancel, log, note, outro, tasks, text } from "@clack/prompts";
4
- import { join } from "pathe";
4
+ import { dirname, join } from "pathe";
5
5
  import fsExtra from "fs-extra";
6
6
  import { addDependency } from "nypm";
7
+ import { pathToFileURL } from "node:url";
7
8
  //#endregion
8
9
  //#region src/constants.ts
9
10
  /** CentoUI current package version */
10
- const VERSION = "0.2.0";
11
+ const VERSION = "0.2.2";
11
12
  /** CentoUI config file name */
12
13
  const CONFIG_FILE_NAME = "centoui.config.ts";
13
14
  /** CentoUI registry file name */
@@ -64,6 +65,36 @@ function validatePath(path) {
64
65
  //#endregion
65
66
  //#region src/utils/file-system-utils.ts
66
67
  /**
68
+ * Resolve the destination path for a component file inside the user's project.
69
+ * Strips the leading `src/components/` registry prefix.
70
+ *
71
+ * e.g. with `componentsDir = './components/ui'`:
72
+ * src/components/button/button.vue → ./components/ui/button/button.vue
73
+ *
74
+ * @param path - The path to the component file in the registry.
75
+ * @param config - The CentoUI configuration.
76
+ * @param cwd - The current working directory.
77
+ * @returns The destination path for the component file.
78
+ */
79
+ function resolveComponentsDestinationPath(path, config, cwd) {
80
+ const normalizedPath = path.replace(/^components\//, "");
81
+ return join(cwd, config.componentsDir, normalizedPath);
82
+ }
83
+ /**
84
+ * Write content to a destination path, creating parent directories as needed.
85
+ *
86
+ * @param path - The path to write the file to.
87
+ * @param content - The content to write to the file.
88
+ */
89
+ async function writeFile(path, content) {
90
+ try {
91
+ await fsExtra.mkdir(dirname(path), { recursive: true });
92
+ await fsExtra.writeFile(path, content, "utf8");
93
+ } catch (error) {
94
+ throw new Error(`Failed to write file: ${path}: ${error}`);
95
+ }
96
+ }
97
+ /**
67
98
  * Ask the user whether to overwrite a path that already exists.
68
99
  * Returns `true` immediately (no prompt) if the file does not exist yet.
69
100
  *
@@ -82,6 +113,22 @@ async function promptOverwrite(label, path) {
82
113
  //#endregion
83
114
  //#region src/utils/config-utils.ts
84
115
  /**
116
+ * Loads the user's CentoUI configuration.
117
+ *
118
+ * @param cwd - The current working directory.
119
+ * @returns The user's CentoUI configuration.
120
+ * @throws If the config file is not found or cannot be loaded.
121
+ */
122
+ async function loadUserConfig(cwd) {
123
+ try {
124
+ const configFilePath = join(cwd, CONFIG_FILE_NAME);
125
+ if (!await fsExtra.pathExists(configFilePath)) throw `No ${CONFIG_FILE_NAME} found. Run \`centoui init\` first.`;
126
+ return (await import(pathToFileURL(configFilePath).href)).default;
127
+ } catch (error) {
128
+ throw new Error(`Failed to load user config: ${error}`);
129
+ }
130
+ }
131
+ /**
85
132
  * Generates the default user-defined CentoUI config file template.
86
133
  * @param themeFilePath - The relative path to the user's theme CSS file.
87
134
  * @param componentsDir - The relative path to the user's components directory.
@@ -121,6 +168,36 @@ async function fetchRegistry() {
121
168
  return registry;
122
169
  }
123
170
  /**
171
+ * Recursively resolves a component and all its transitive dependencies.
172
+ *
173
+ * @param name - Root component name to resolve
174
+ * @param registry - The pre-fetched registry index
175
+ * @param seen - Internal set used to prevent infinite loops on circular deps
176
+ * @returns Map of component name → registry entry for the full dependency tree
177
+ */
178
+ function resolveComponentTree(name, registry, seen = /* @__PURE__ */ new Set()) {
179
+ const result = /* @__PURE__ */ new Map();
180
+ if (seen.has(name)) return result;
181
+ seen.add(name);
182
+ const entry = registry.components.find((component) => component.name === name);
183
+ if (!entry) throw new Error(`Component "${name}" not found in registry.`);
184
+ result.set(name, entry);
185
+ for (const dependency of entry.componentDeps) for (const [dependencyName, dependencyEntry] of resolveComponentTree(dependency, registry, seen)) result.set(dependencyName, dependencyEntry);
186
+ return result;
187
+ }
188
+ /**
189
+ * Fetches the raw source code of a component or utility file from the registry.
190
+ *
191
+ * @param path - Path as listed in the component registry (e.g. 'components/button/button.vue')
192
+ * @returns The raw source code of the file
193
+ */
194
+ async function fetchComponentFile(path) {
195
+ const requestUrl = `${BASE_URL}/${path}`;
196
+ const response = await fetch(requestUrl, { headers: FETCH_HEADERS });
197
+ if (!response.ok) throw new Error(`${response.status}: ${response.statusText}`);
198
+ return response.text();
199
+ }
200
+ /**
124
201
  * Fetches the theme file from the registry.
125
202
  *
126
203
  * @returns The raw source code of the theme file
@@ -220,13 +297,99 @@ function init() {
220
297
  });
221
298
  }
222
299
  //#endregion
300
+ //#region src/utils/components-utils.ts
301
+ /**
302
+ * Resolves the absolute path to a specific component directory.
303
+ *
304
+ * @param name - The name of the component (kebab-case)
305
+ * @param config - CentoUI configuration
306
+ * @param cwd - Current working directory
307
+ * @returns The absolute path to the component directory
308
+ */
309
+ function getComponentPath(name, config, cwd) {
310
+ return join(cwd, config.componentsDir, name);
311
+ }
312
+ //#endregion
313
+ //#region src/commands/add.ts
314
+ /**
315
+ * Command: add
316
+ *
317
+ * Installs one or more components (and their transitive dependencies) from
318
+ * the registry into the user's project.
319
+ */
320
+ function add() {
321
+ return defineCommand({
322
+ meta: {
323
+ name: "add",
324
+ description: "Add one or more components to your project"
325
+ },
326
+ args: {},
327
+ async run({ args }) {
328
+ try {
329
+ const cwd = process.cwd();
330
+ const componentNames = args._;
331
+ intro("CentoUI — Add components");
332
+ if (componentNames.length === 0) {
333
+ log.error("No components specified. Usage: centoui add <component> [component...]");
334
+ process.exit(1);
335
+ }
336
+ const config = await loadUserConfig(cwd);
337
+ const registry = await fetchRegistry();
338
+ const allComponents = /* @__PURE__ */ new Map();
339
+ for (const name of componentNames) try {
340
+ const tree = resolveComponentTree(name, registry);
341
+ for (const [depName, depEntry] of tree) allComponents.set(depName, depEntry);
342
+ } catch (error) {
343
+ log.error(`Failed to resolve "${name}": ${error}`);
344
+ process.exit(1);
345
+ }
346
+ const installDecisions = /* @__PURE__ */ new Map();
347
+ for (const [name] of allComponents) {
348
+ const shouldWrite = await promptOverwrite(name, getComponentPath(name, config, cwd));
349
+ installDecisions.set(name, shouldWrite);
350
+ }
351
+ const allPackageDeps = {};
352
+ for (const [name, entry] of allComponents) if (installDecisions.get(name)) Object.assign(allPackageDeps, entry.packageDeps);
353
+ const componentsToInstall = Array.from(allComponents.entries()).filter(([name]) => installDecisions.get(name));
354
+ await tasks([...componentsToInstall.map(([name, entry]) => ({
355
+ title: `Installing ${name}`,
356
+ task: async () => {
357
+ for (const path of entry.files) {
358
+ const content = await fetchComponentFile(path);
359
+ await writeFile(resolveComponentsDestinationPath(path, config, cwd), content);
360
+ }
361
+ return `${name} installed (${entry.files.length} file${entry.files.length !== 1 ? "s" : ""})`;
362
+ }
363
+ })), {
364
+ title: "Installing packages",
365
+ task: async (message) => installPackages(allPackageDeps, cwd, message)
366
+ }]);
367
+ const skipped = Array.from(installDecisions.entries()).filter(([, shouldWrite]) => !shouldWrite).map(([name]) => name);
368
+ note([
369
+ `Installed > ${componentsToInstall.map(([name]) => name).join(", ") || "none"}`,
370
+ skipped.length > 0 ? `Skipped > ${skipped.join(", ")}` : "",
371
+ "",
372
+ "Import components from your components directory to use them"
373
+ ].filter(Boolean).join("\n"), "Components added");
374
+ outro("All set!");
375
+ } catch (error) {
376
+ log.error(`Failed to add component(s): ${error}`);
377
+ process.exit(1);
378
+ }
379
+ }
380
+ });
381
+ }
382
+ //#endregion
223
383
  //#region src/index.ts
224
384
  runMain(defineCommand({
225
385
  meta: {
226
386
  name: "centoui",
227
387
  version: VERSION
228
388
  },
229
- subCommands: { init }
389
+ subCommands: {
390
+ init,
391
+ add
392
+ }
230
393
  }));
231
394
  //#endregion
232
395
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "centoui-cli",
3
3
  "type": "module",
4
- "version": "0.2.0",
4
+ "version": "0.2.2",
5
5
  "private": false,
6
6
  "description": "Official CLI for CentoUI.",
7
7
  "author": "Favour Emeka <favorodera@gmail.com>",