centoui-cli 1.0.0-alpha.9 → 1.0.0
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 +95 -23
- package/dist/index.d.mts +31 -59
- package/dist/index.mjs +342 -509
- package/package.json +25 -16
package/README.md
CHANGED
|
@@ -1,44 +1,116 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>centoui-cli</h1>
|
|
3
|
+
<p><strong>Manage CentoUI components from the terminal.</strong></p>
|
|
4
|
+
<p>
|
|
5
|
+
<a href="https://npmx.dev/package/centoui-cli"><img src="https://img.shields.io/npm/v/centoui-cli.svg?style=plastic&label=NPM%20Version&color=blue" alt="NPM Version"></a>
|
|
6
|
+
<a href="https://npmx.dev/package/centoui-cli"><img src="https://img.shields.io/npm/dw/centoui-cli.svg?style=plastic&label=NPM%20Downloads&color=blue" alt="NPM Downloads"></a>
|
|
7
|
+
<a href="https://npmx.dev/package/centoui-cli"><img src="https://img.shields.io/npm/unpacked-size/centoui-cli?style=plastic&label=NPM%20Unpacked%20Size&color=blue" alt="NPM Unpacked Size"></a>
|
|
8
|
+
</p>
|
|
9
|
+
</div>
|
|
2
10
|
|
|
3
|
-
|
|
4
|
-
[](https://www.npmjs.com/package/centoui-cli)
|
|
5
|
-
[](https://github.com/favorodera/centoui/blob/main/LICENSE)
|
|
11
|
+
<br>
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
`centoui-cli` is the official command-line interface for [CentoUI](https://github.com/favorodera/centoui). It allows you to initialize CentoUI in your project, add new components, and manage their versions directly from your terminal.
|
|
13
|
+
`centoui-cli` is the command-line interface for [CentoUI](../core). It initializes projects, pulls component source files from the registry into your codebase, resolves dependency trees automatically, and cleans up when you remove components.
|
|
10
14
|
|
|
11
15
|
## Commands
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
- **`add [component]`**: Add specific components to your project. Peer dependencies install automatically.
|
|
15
|
-
- **`remove [component]`**: Cleanly remove components and their dependencies.
|
|
17
|
+
### `centoui init`
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
Scaffolds a new CentoUI project in your current directory.
|
|
18
20
|
|
|
19
21
|
```bash
|
|
20
|
-
# Initialize CentoUI in your project
|
|
21
22
|
pnpm dlx centoui init
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**What it does:**
|
|
26
|
+
|
|
27
|
+
1. Prompts you for a component directory (default: `src/components/centoui`), a utils file path (default: `src/utils/centoui-utils.ts`), and theme CSS path (default: `src/assets/css/centoui.css`).
|
|
28
|
+
2. Writes `centoui.config.ts` with your chosen paths and default icon mappings.
|
|
29
|
+
3. Fetches and writes the `centoui.css` theme file with all light/dark color tokens.
|
|
30
|
+
4. Writes the `centoui-utils.ts` file with all utility functions.
|
|
31
|
+
5. Creates the component directory.
|
|
32
|
+
6. Installs global peer dependencies (`vue`, `reka-ui`, `tailwindcss`, etc.).
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
### `centoui add <component> [component...]`
|
|
37
|
+
|
|
38
|
+
Adds one or more components to your project.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pnpm dlx centoui add button accordion select
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**What it does:**
|
|
45
|
+
|
|
46
|
+
1. Fetches the component registry from GitHub.
|
|
47
|
+
2. Resolves the full dependency tree — if `select` depends on `popover`, both are installed.
|
|
48
|
+
3. Asks before overwriting any component that already exists.
|
|
49
|
+
4. Downloads `.vue` and `index.ts` files for each component into your configured directory.
|
|
50
|
+
5. Installs any npm packages required by the added components.
|
|
22
51
|
|
|
23
|
-
|
|
24
|
-
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### `centoui remove <component>`
|
|
55
|
+
|
|
56
|
+
Removes an installed component and cleans up orphaned packages.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pnpm dlx centoui remove accordion
|
|
25
60
|
```
|
|
26
61
|
|
|
62
|
+
**What it does:**
|
|
63
|
+
|
|
64
|
+
1. Checks that the component is actually installed.
|
|
65
|
+
2. Blocks removal if other installed components depend on it (e.g., you can't remove `popover` while `select` is installed).
|
|
66
|
+
3. Deletes the component's directory.
|
|
67
|
+
4. Uninstalls npm packages that are no longer needed by any remaining component.
|
|
68
|
+
|
|
69
|
+
## Version-Locked Asset Fetching
|
|
70
|
+
|
|
71
|
+
Every network request the CLI makes — the registry index, individual component files, the theme CSS, and the default config — is resolved against the **exact git tag that matches the installed version of `centoui-cli`**.
|
|
72
|
+
|
|
73
|
+
The base URL for all assets is derived directly from the CLI's own `package.json` version at build time:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
https://raw.githubusercontent.com/favorodera/centoui/refs/tags/v<VERSION>/packages/core/src
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This means:
|
|
80
|
+
|
|
81
|
+
- If you have `centoui-cli@1.2.3` installed, all fetched assets come from the `v1.2.3` tag of the `centoui` repository — never from `main` or any other release.
|
|
82
|
+
- The registry, component source files, CSS theme, and config defaults are always in sync with each other and with the CLI you are running.
|
|
83
|
+
- Upgrading the CLI upgrades the version tag automatically — no separate step needed.
|
|
84
|
+
|
|
85
|
+
This design eliminates an entire class of version mismatch bugs where the CLI and the component source could drift out of sync.
|
|
86
|
+
|
|
27
87
|
## Configuration
|
|
28
88
|
|
|
29
|
-
|
|
89
|
+
After running `centoui init`, your project root will contain a `centoui.config.ts`:
|
|
30
90
|
|
|
31
91
|
```ts
|
|
32
|
-
import { defineConfig } from
|
|
92
|
+
import { defineConfig } from 'centoui'
|
|
33
93
|
|
|
34
94
|
export default defineConfig({
|
|
35
|
-
|
|
36
|
-
componentsDir: "./src/components/centoui",
|
|
37
|
-
themeFilePath: "./src/assets/css/centoui.css",
|
|
95
|
+
componentsDir: './src/components/centoui',
|
|
38
96
|
icons: {
|
|
39
|
-
check:
|
|
40
|
-
|
|
41
|
-
|
|
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',
|
|
42
106
|
},
|
|
107
|
+
themeFilePath: './src/assets/css/centoui.css',
|
|
108
|
+
utilsFilePath: './src/utils/centoui-utils.ts',
|
|
43
109
|
})
|
|
44
|
-
```
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The `icons` map lets you swap icon libraries without touching component code. Components reference icons by their slot name (e.g., `check`, `close`), and the config resolves them to [Iconify](https://iconify.design/) IDs.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
[MIT](../../LICENSE) © [Favour Emeka](https://github.com/favorodera)
|
package/dist/index.d.mts
CHANGED
|
@@ -1,63 +1,35 @@
|
|
|
1
1
|
//#region src/types.d.ts
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
/** The CentoUI version this project was initialised with. */version: string; /** Relative path (from project root) to the directory where components are installed. */
|
|
52
|
-
componentsDir: string; /** Relative path (from project root) to the CSS file that receives theme and component styles. */
|
|
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. */
|
|
53
25
|
themeFilePath: 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
|
-
*/
|
|
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,
|
|
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,
|
|
4
|
-
import {
|
|
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 { pathToFileURL } from "node:url";
|
|
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
|
|
10
|
+
const VERSION = "1.0.0";
|
|
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,14 +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
|
|
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}/css/centoui.css`;
|
|
20
|
+
const BASE_URL = `https://raw.githubusercontent.com/favorodera/centoui/refs/tags/v${VERSION}/packages/core/src`;
|
|
29
21
|
/**
|
|
30
22
|
* HTTP headers required when fetching raw content from the GitHub API.
|
|
31
23
|
* These ensure we get the raw file bytes, not GitHub's HTML wrapper.
|
|
@@ -35,593 +27,434 @@ const GITHUB_RAW_FETCH_HEADERS = {
|
|
|
35
27
|
"X-GitHub-Api-Version": "2026-03-10"
|
|
36
28
|
};
|
|
37
29
|
//#endregion
|
|
38
|
-
//#region src/utils/
|
|
30
|
+
//#region src/utils/network.ts
|
|
39
31
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* @param requiredPackages - Map of `packageName → semver version range`.
|
|
48
|
-
* @param cwd - Absolute path to the project root (must contain `package.json`).
|
|
49
|
-
* @param onProgress - Optional callback fired before each `addDependency` call
|
|
50
|
-
* with a `"[n/total] package@version"` string.
|
|
51
|
-
* @returns Human-readable summary (e.g. `"Installed 3 package(s)"`).
|
|
52
|
-
* @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.
|
|
53
38
|
*/
|
|
54
|
-
async function
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+
}
|
|
60
47
|
};
|
|
61
|
-
|
|
62
|
-
if (packagesToInstall.length === 0) return "All packages already up to date";
|
|
48
|
+
let response;
|
|
63
49
|
try {
|
|
64
|
-
|
|
65
|
-
onProgress?.(`[${index + 1}/${packagesToInstall.length}] ${pkg}`);
|
|
66
|
-
await addDependency(pkg, {
|
|
67
|
-
cwd,
|
|
68
|
-
silent: true
|
|
69
|
-
});
|
|
70
|
-
}
|
|
50
|
+
response = await fetch(url, resolvedInit);
|
|
71
51
|
} catch (error) {
|
|
72
|
-
throw new Error(
|
|
52
|
+
throw new Error("Network request failed", { cause: error });
|
|
53
|
+
}
|
|
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;
|
|
73
63
|
}
|
|
74
|
-
return
|
|
64
|
+
return resolvedResponse;
|
|
75
65
|
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/utils/config.ts
|
|
76
68
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* dependencies are never removed prematurely.
|
|
82
|
-
*
|
|
83
|
-
* @param packagesToConsider - Packages belonging to the component being removed.
|
|
84
|
-
* @param packagesStillNeeded - Union of `packageDeps` from all other installed components.
|
|
85
|
-
* @param cwd - Absolute path to the project root.
|
|
86
|
-
* @param onProgress - Optional callback fired before each `removeDependency` call.
|
|
87
|
-
* @returns Human-readable summary (e.g. `"Removed 2 package(s)"`).
|
|
88
|
-
* @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.
|
|
89
73
|
*/
|
|
90
|
-
async function
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
cwd,
|
|
98
|
-
silent: true
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
} catch (error) {
|
|
102
|
-
throw new Error(`[removeOrphanedPackages] Failed to remove packages: ${error}`);
|
|
103
|
-
}
|
|
104
|
-
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;
|
|
105
81
|
}
|
|
106
82
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* @param value - The raw value from the prompt.
|
|
112
|
-
* @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.
|
|
113
87
|
*/
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
}
|
|
117
104
|
}
|
|
118
105
|
//#endregion
|
|
119
|
-
//#region src/utils/file-system
|
|
106
|
+
//#region src/utils/file-system.ts
|
|
120
107
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* `"components/button/button.vue"`). This function strips that leading segment
|
|
126
|
-
* and joins the remainder with the user's configured components directory so
|
|
127
|
-
* that `"components/button/button.vue"` becomes, for example,
|
|
128
|
-
* `"/home/user/my-app/src/components/centoui/button/button.vue"`.
|
|
129
|
-
*
|
|
130
|
-
* @param registryFilePath - Path as it appears in the component's registry entry
|
|
131
|
-
* (always starts with `"components/"`).
|
|
132
|
-
* @param config - The loaded CentoUI project configuration.
|
|
133
|
-
* @param cwd - Absolute path to the project root.
|
|
134
|
-
* @returns Absolute destination path for the file in the user's project.
|
|
135
|
-
*
|
|
136
|
-
* @example
|
|
137
|
-
* // config.componentsDir = 'src/components/centoui', cwd = '/home/user/my-app'
|
|
138
|
-
* mapRegistryPathToProjectDest('components/button/button.vue', config, cwd)
|
|
139
|
-
* // → '/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.
|
|
140
112
|
*/
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
}
|
|
144
119
|
}
|
|
145
120
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* target directory tree exists so callers don't have to `mkdir` manually.
|
|
150
|
-
*
|
|
151
|
-
* @param filePath - Absolute path where the file should be written.
|
|
152
|
-
* @param content - UTF-8 string content to write.
|
|
153
|
-
* @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.
|
|
154
124
|
*/
|
|
155
|
-
async function
|
|
125
|
+
async function createDirectory(path) {
|
|
156
126
|
try {
|
|
157
|
-
await fsExtra.
|
|
158
|
-
await fsExtra.writeFile(filePath, content, "utf8");
|
|
127
|
+
await fsExtra.ensureDir(path);
|
|
159
128
|
} catch (error) {
|
|
160
|
-
throw new Error(`
|
|
129
|
+
throw new Error(`Failed to create directory ${path}`, { cause: error });
|
|
161
130
|
}
|
|
162
131
|
}
|
|
163
132
|
/**
|
|
164
133
|
* Prompts the user to confirm before overwriting an existing path.
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* any prompt — nothing to overwrite, safe to proceed.
|
|
168
|
-
*
|
|
169
|
-
* If the user cancels the prompt (e.g. Ctrl+C), the process exits cleanly
|
|
170
|
-
* with a cancellation message rather than throwing.
|
|
171
|
-
*
|
|
172
|
-
* @param label - Human-readable name shown in the prompt (e.g. `"centoui.config.ts"`).
|
|
173
|
-
* @param path - Absolute path to the file or directory to potentially overwrite.
|
|
174
|
-
* @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.
|
|
175
136
|
*/
|
|
176
|
-
async function
|
|
137
|
+
async function confirmOverwrite(path) {
|
|
177
138
|
if (!await fsExtra.pathExists(path)) return true;
|
|
178
|
-
const answer = await confirm({
|
|
139
|
+
const answer = await confirm({
|
|
140
|
+
initialValue: false,
|
|
141
|
+
message: `${path} already exists. Overwrite?`
|
|
142
|
+
});
|
|
179
143
|
if (isCancel(answer)) {
|
|
180
|
-
cancel("Operation cancelled.");
|
|
144
|
+
cancel("Operation cancelled by user.");
|
|
181
145
|
process.exit(0);
|
|
182
146
|
}
|
|
183
147
|
return answer;
|
|
184
148
|
}
|
|
185
149
|
//#endregion
|
|
186
|
-
//#region src/utils/
|
|
150
|
+
//#region src/utils/package.ts
|
|
187
151
|
/**
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* @param cwd - Absolute path to the project root.
|
|
194
|
-
* @returns The default export of `centoui.config.ts` cast as {@link CentoUIConfig}.
|
|
195
|
-
* @throws If `centoui.config.ts` is not found — instructs the user to run `centoui init`.
|
|
196
|
-
* @throws If the file exists but cannot be imported or does not export a default value.
|
|
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.
|
|
197
157
|
*/
|
|
198
|
-
async function
|
|
199
|
-
const configFilePath = join(cwd, CONFIG_FILE_NAME);
|
|
158
|
+
async function installDependency(name, version, cwd) {
|
|
200
159
|
try {
|
|
201
|
-
|
|
202
|
-
|
|
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.`;
|
|
203
171
|
} catch (error) {
|
|
204
|
-
throw new Error(`
|
|
172
|
+
throw new Error(`Failed to install ${name}`, { cause: error });
|
|
205
173
|
}
|
|
206
174
|
}
|
|
207
175
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* for the built-in Lucide icon set.
|
|
213
|
-
*
|
|
214
|
-
* @param themeFilePath - Relative path (from project root) where the CSS theme file lives.
|
|
215
|
-
* @param componentsDir - Relative path (from project root) where components will be installed.
|
|
216
|
-
* @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.
|
|
217
180
|
*/
|
|
218
|
-
function
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
})
|
|
231
|
-
|
|
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
|
+
}
|
|
232
196
|
}
|
|
233
197
|
//#endregion
|
|
234
|
-
//#region src/utils/registry
|
|
198
|
+
//#region src/utils/registry.ts
|
|
235
199
|
/** In-process cache so the registry is only fetched once per CLI invocation. */
|
|
236
|
-
let cachedRegistry
|
|
200
|
+
let cachedRegistry;
|
|
237
201
|
/**
|
|
238
|
-
* Fetches the
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* Every other registry utility calls this internally, so the network round-trip
|
|
242
|
-
* only happens once regardless of how many components are being installed.
|
|
243
|
-
*
|
|
244
|
-
* @returns The full registry object including globals and all component entries.
|
|
245
|
-
* @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.
|
|
246
204
|
*/
|
|
247
|
-
async function
|
|
205
|
+
async function fetchRegistry() {
|
|
248
206
|
if (cachedRegistry) return cachedRegistry;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
} catch (error) {
|
|
253
|
-
throw new Error(`[fetchFullRegistry] Network request to registry failed: ${error}`);
|
|
254
|
-
}
|
|
255
|
-
if (!response.ok) throw new Error(`[fetchFullRegistry] Registry responded with ${response.status} ${response.statusText} (URL: ${REGISTRY_INDEX_URL})`);
|
|
256
|
-
cachedRegistry = await response.json();
|
|
257
|
-
return cachedRegistry;
|
|
207
|
+
const registry = await sendNetworkRequest("/registry/index.json", "json");
|
|
208
|
+
cachedRegistry = registry;
|
|
209
|
+
return registry;
|
|
258
210
|
}
|
|
259
211
|
/**
|
|
260
|
-
* Recursively resolves a component and
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* @param componentName - Root component name to start resolving from.
|
|
267
|
-
* @param registry - Pre-fetched registry (pass the result of {@link fetchFullRegistry}).
|
|
268
|
-
* @param visited - Internal visited set used to prevent infinite loops;
|
|
269
|
-
* callers should omit this — it defaults to an empty set.
|
|
270
|
-
* @returns A `Map<componentName, ComponentRegistry>` covering the full tree.
|
|
271
|
-
* @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.
|
|
272
218
|
*/
|
|
273
|
-
function
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
result.set(componentName, entry);
|
|
280
|
-
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);
|
|
281
225
|
return result;
|
|
282
226
|
}
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/commands/add.ts
|
|
283
229
|
/**
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
* Registry file paths (e.g. `"components/button/button.vue"`) are relative
|
|
288
|
-
* to `packages/core/src/`, which is already the base of {@link CORE_SRC_BASE_URL}.
|
|
289
|
-
* This function simply appends the path and downloads the content.
|
|
290
|
-
*
|
|
291
|
-
* @param registryFilePath - Path as it appears in the component's `files` array
|
|
292
|
-
* (e.g. `"components/button/button.vue"`).
|
|
293
|
-
* @returns Raw UTF-8 content of the file.
|
|
294
|
-
* @throws If the network request fails or the server returns a non-2xx status.
|
|
295
|
-
*/
|
|
296
|
-
async function fetchRegistryFileContent(registryFilePath) {
|
|
297
|
-
const url = `${CORE_SRC_BASE_URL}/${registryFilePath}`;
|
|
298
|
-
let response;
|
|
299
|
-
try {
|
|
300
|
-
response = await fetch(url, { headers: GITHUB_RAW_FETCH_HEADERS });
|
|
301
|
-
} catch (error) {
|
|
302
|
-
throw new Error(`[fetchRegistryFileContent] Network request failed for "${registryFilePath}": ${error}`);
|
|
303
|
-
}
|
|
304
|
-
if (!response.ok) throw new Error(`[fetchRegistryFileContent] Server returned ${response.status} ${response.statusText} for "${url}"`);
|
|
305
|
-
return response.text();
|
|
306
|
-
}
|
|
307
|
-
/**
|
|
308
|
-
* Fetches the raw content of the CentoUI CSS theme file from GitHub.
|
|
309
|
-
*
|
|
310
|
-
* This is the file written to the user's project during `centoui init` and
|
|
311
|
-
* contains all CSS custom properties and base styles for every component.
|
|
312
|
-
*
|
|
313
|
-
* @returns Raw UTF-8 content of the theme CSS file.
|
|
314
|
-
* @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.
|
|
315
232
|
*/
|
|
316
|
-
async function
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
+
});
|
|
325
291
|
}
|
|
326
292
|
//#endregion
|
|
327
293
|
//#region src/commands/init.ts
|
|
328
294
|
/**
|
|
329
|
-
* Command: `centoui init`
|
|
330
|
-
*
|
|
331
295
|
* Bootstraps a new CentoUI project in the current working directory.
|
|
332
|
-
*
|
|
333
|
-
* Flow:
|
|
334
|
-
* 1. Prompt the user for the components directory and theme CSS file path.
|
|
335
|
-
* 2. Ask upfront whether to overwrite any of the three output paths
|
|
336
|
-
* (config file, theme CSS, components directory) if they already exist.
|
|
337
|
-
* 3. Write the config file, fetch and write the theme CSS, prepare the
|
|
338
|
-
* components directory, and install global npm dependencies.
|
|
296
|
+
* @returns The Citty command definition that executes the 'init' CLI process.
|
|
339
297
|
*/
|
|
340
298
|
function init() {
|
|
341
299
|
return defineCommand({
|
|
342
300
|
meta: {
|
|
343
|
-
|
|
344
|
-
|
|
301
|
+
description: "Initialize a new CentoUI project",
|
|
302
|
+
name: "init"
|
|
345
303
|
},
|
|
346
|
-
async
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
return `${CONFIG_FILE_NAME} written`;
|
|
379
|
-
}
|
|
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!";
|
|
380
336
|
},
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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!";
|
|
389
347
|
},
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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!";
|
|
356
|
+
},
|
|
357
|
+
title: "Preparing components directory."
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
task: async () => {
|
|
361
|
+
registry = await fetchRegistry();
|
|
362
|
+
return "Registry loaded!";
|
|
397
363
|
},
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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);
|
|
403
371
|
}
|
|
372
|
+
return "Dependencies installed!";
|
|
404
373
|
},
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
]);
|
|
410
|
-
note([
|
|
411
|
-
`Config > ${configPath}`,
|
|
412
|
-
`Theme > ${themePath}`,
|
|
413
|
-
`Components > ${componentsPath}`,
|
|
414
|
-
"",
|
|
415
|
-
"Run 'centoui add button' to install your first component."
|
|
416
|
-
].filter(Boolean).join("\n"), "CentoUI initialized");
|
|
417
|
-
outro("All set!");
|
|
418
|
-
} catch (error) {
|
|
419
|
-
log.error(`Initialization failed: ${error}`);
|
|
420
|
-
process.exit(1);
|
|
421
|
-
}
|
|
374
|
+
title: "Installing dependencies."
|
|
375
|
+
}
|
|
376
|
+
]);
|
|
377
|
+
outro("Initialization Complete!");
|
|
422
378
|
}
|
|
423
379
|
});
|
|
424
380
|
}
|
|
425
381
|
//#endregion
|
|
426
|
-
//#region src/utils/components
|
|
427
|
-
/**
|
|
428
|
-
* Scans the configured components directory and returns a sorted list of
|
|
429
|
-
* installed component names.
|
|
430
|
-
*
|
|
431
|
-
* Each direct subdirectory of `config.componentsDir` is treated as one
|
|
432
|
-
* installed component. Non-directory entries (loose files) are ignored.
|
|
433
|
-
*
|
|
434
|
-
* @param config - The loaded CentoUI project configuration.
|
|
435
|
-
* @param cwd - Absolute path to the project root.
|
|
436
|
-
* @returns Sorted array of installed component names, or an empty array if
|
|
437
|
-
* the components directory does not exist yet.
|
|
438
|
-
* @throws If the directory exists but cannot be read.
|
|
439
|
-
*/
|
|
440
|
-
async function listInstalledComponentNames(config, cwd) {
|
|
441
|
-
const componentsDir = join(cwd, config.componentsDir);
|
|
442
|
-
try {
|
|
443
|
-
if (!await fsExtra.pathExists(componentsDir)) return [];
|
|
444
|
-
return (await fsExtra.readdir(componentsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
445
|
-
} catch (error) {
|
|
446
|
-
throw new Error(`[listInstalledComponentNames] Failed to read components directory "${componentsDir}": ${error}`);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
/**
|
|
450
|
-
* Returns the absolute path to a component's installation directory inside
|
|
451
|
-
* the user's project.
|
|
452
|
-
*
|
|
453
|
-
* The directory may or may not exist yet — this function does not check.
|
|
454
|
-
* Use {@link checkIsComponentInstalled} if you need to verify existence.
|
|
455
|
-
*
|
|
456
|
-
* @param componentName - The component name in kebab-case (e.g. `"button"`).
|
|
457
|
-
* @param config - The loaded CentoUI project configuration.
|
|
458
|
-
* @param cwd - Absolute path to the project root.
|
|
459
|
-
*/
|
|
460
|
-
function resolveComponentInstallDir(componentName, config, cwd) {
|
|
461
|
-
return join(cwd, config.componentsDir, componentName);
|
|
462
|
-
}
|
|
382
|
+
//#region src/utils/components.ts
|
|
463
383
|
/**
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
* @
|
|
468
|
-
* @
|
|
469
|
-
* @param cwd - Absolute path to the project root.
|
|
470
|
-
* @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.
|
|
471
389
|
*/
|
|
472
|
-
async function
|
|
473
|
-
const componentInstallDir = resolveComponentInstallDir(componentName, config, cwd);
|
|
390
|
+
async function getInstalledComponents(config, cwd) {
|
|
474
391
|
try {
|
|
475
|
-
|
|
392
|
+
const componentsDir = join(cwd, config.componentsDir);
|
|
393
|
+
return (await fsExtra.readdir(componentsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).toSorted();
|
|
476
394
|
} catch (error) {
|
|
477
|
-
throw new Error(
|
|
395
|
+
throw new Error("Failed to read components directory", { cause: error });
|
|
478
396
|
}
|
|
479
397
|
}
|
|
480
398
|
//#endregion
|
|
481
|
-
//#region src/commands/add.ts
|
|
482
|
-
/**
|
|
483
|
-
* Command: `centoui add <component> [component...]`
|
|
484
|
-
*
|
|
485
|
-
* Installs one or more components — and their full transitive dependency trees
|
|
486
|
-
* — from the registry into the user's project.
|
|
487
|
-
*
|
|
488
|
-
* Flow:
|
|
489
|
-
* 1. Resolve the full dependency tree for every requested component.
|
|
490
|
-
* 2. Ask the user upfront whether to overwrite any that already exist.
|
|
491
|
-
* 3. Fetch and write the source files for components the user approved.
|
|
492
|
-
* 4. Install any npm packages required by the components being written.
|
|
493
|
-
*/
|
|
494
|
-
function add() {
|
|
495
|
-
return defineCommand({
|
|
496
|
-
meta: {
|
|
497
|
-
name: "add",
|
|
498
|
-
description: "Add one or more components to your project"
|
|
499
|
-
},
|
|
500
|
-
args: {},
|
|
501
|
-
async run({ args }) {
|
|
502
|
-
try {
|
|
503
|
-
const cwd = process.cwd();
|
|
504
|
-
const requestedNames = args._;
|
|
505
|
-
intro("CentoUI — Add components");
|
|
506
|
-
if (requestedNames.length === 0) throw new Error("No components specified. Usage: centoui add <component> [component...]");
|
|
507
|
-
const config = await loadCentoUIConfig(cwd);
|
|
508
|
-
const registry = await fetchFullRegistry();
|
|
509
|
-
const allComponents = /* @__PURE__ */ new Map();
|
|
510
|
-
for (const name of requestedNames) try {
|
|
511
|
-
const tree = resolveComponentWithDependencies(name, registry);
|
|
512
|
-
for (const [depName, depEntry] of tree) allComponents.set(depName, depEntry);
|
|
513
|
-
} catch (error) {
|
|
514
|
-
throw new Error(`Failed to resolve "${name}": ${error}`);
|
|
515
|
-
}
|
|
516
|
-
const writeDecisions = /* @__PURE__ */ new Map();
|
|
517
|
-
for (const [name] of allComponents) {
|
|
518
|
-
const shouldWrite = await confirmOverwriteIfExists(name, resolveComponentInstallDir(name, config, cwd));
|
|
519
|
-
writeDecisions.set(name, shouldWrite);
|
|
520
|
-
}
|
|
521
|
-
const packageDepsToInstall = {};
|
|
522
|
-
for (const [name, entry] of allComponents) if (writeDecisions.get(name)) Object.assign(packageDepsToInstall, entry.packageDeps);
|
|
523
|
-
const approvedComponents = Array.from(allComponents.entries()).filter(([name]) => writeDecisions.get(name));
|
|
524
|
-
await tasks([...approvedComponents.map(([name, entry]) => ({
|
|
525
|
-
title: `Installing ${name}`,
|
|
526
|
-
task: async () => {
|
|
527
|
-
for (const registryFilePath of entry.files) {
|
|
528
|
-
const content = await fetchRegistryFileContent(registryFilePath);
|
|
529
|
-
await writeFileWithDirs(mapRegistryPathToProjectDest(registryFilePath, config, cwd), content);
|
|
530
|
-
}
|
|
531
|
-
return `${name} installed (${entry.files.length} file(s))`;
|
|
532
|
-
}
|
|
533
|
-
})), {
|
|
534
|
-
title: "Installing packages",
|
|
535
|
-
task: async (message) => installMissingPackages(packageDepsToInstall, cwd, message)
|
|
536
|
-
}]);
|
|
537
|
-
const skippedNames = Array.from(writeDecisions.entries()).filter(([, shouldWrite]) => !shouldWrite).map(([name]) => name);
|
|
538
|
-
note([
|
|
539
|
-
`Installed > ${approvedComponents.map(([name]) => name).join(", ") || "none"}`,
|
|
540
|
-
skippedNames.length > 0 ? `Skipped > ${skippedNames.join(", ")}` : "",
|
|
541
|
-
"",
|
|
542
|
-
"Import components from your components directory to use them."
|
|
543
|
-
].filter(Boolean).join("\n"), "Component(s) added");
|
|
544
|
-
outro("All set!");
|
|
545
|
-
} catch (error) {
|
|
546
|
-
log.error(`Failed to add component(s): ${error}`);
|
|
547
|
-
process.exit(1);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
//#endregion
|
|
553
399
|
//#region src/commands/remove.ts
|
|
554
400
|
/**
|
|
555
|
-
* Command: `centoui remove <component>`
|
|
556
|
-
*
|
|
557
401
|
* Removes a single installed component and its files from the user's project.
|
|
558
|
-
*
|
|
559
|
-
* Flow:
|
|
560
|
-
* 1. Confirm the component is actually installed.
|
|
561
|
-
* 2. Fetch the registry and check whether any other installed components
|
|
562
|
-
* declare this one as a `componentDep` — block removal if so.
|
|
563
|
-
* 3. Collect the packages still required by remaining components so we
|
|
564
|
-
* know which of this component's packages become orphaned.
|
|
565
|
-
* 4. Ask for confirmation, then delete the component directory and remove
|
|
566
|
-
* any newly orphaned npm packages.
|
|
402
|
+
* @returns The Citty command definition that executes the 'remove' CLI process.
|
|
567
403
|
*/
|
|
568
404
|
function remove() {
|
|
569
405
|
return defineCommand({
|
|
570
406
|
meta: {
|
|
571
|
-
|
|
572
|
-
|
|
407
|
+
description: "Remove an installed component from your project",
|
|
408
|
+
name: "remove"
|
|
573
409
|
},
|
|
574
410
|
args: { component: {
|
|
575
|
-
|
|
411
|
+
description: "Name of the component to remove (e.g. \"button\")",
|
|
576
412
|
required: true,
|
|
577
|
-
|
|
413
|
+
type: "positional"
|
|
578
414
|
} },
|
|
579
|
-
async
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const list = Array.from(dependents).map((dependent) => ` · ${dependent}`).join("\n");
|
|
601
|
-
throw new Error(`Cannot remove "${componentName}" — the following installed components depend on it:\n${list}`);
|
|
602
|
-
}
|
|
603
|
-
const confirmed = await confirm({ message: `Remove "${componentName}"?` });
|
|
604
|
-
if (isCancel(confirmed) || !confirmed) {
|
|
605
|
-
cancel("Removal cancelled.");
|
|
606
|
-
process.exit(0);
|
|
607
|
-
}
|
|
608
|
-
await tasks([{
|
|
609
|
-
title: `Removing ${componentName}`,
|
|
610
|
-
task: async () => {
|
|
611
|
-
await fsExtra.remove(componentInstallDir);
|
|
612
|
-
return `${componentName} removed`;
|
|
613
|
-
}
|
|
614
|
-
}, {
|
|
615
|
-
title: "Removing orphaned packages",
|
|
616
|
-
task: async (message) => removeOrphanedPackages(targetEntry.packageDeps, packagesStillNeeded, cwd, message)
|
|
617
|
-
}]);
|
|
618
|
-
const removedPackages = Object.keys(targetEntry.packageDeps).filter((pkg) => !(pkg in packagesStillNeeded));
|
|
619
|
-
if (removedPackages.length > 0) note(removedPackages.map((pkg) => ` · ${pkg}`).join("\n"), "Packages removed");
|
|
620
|
-
outro("All set!");
|
|
621
|
-
} catch (error) {
|
|
622
|
-
log.error(`Failed to remove component: ${error}`);
|
|
623
|
-
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 || {});
|
|
624
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!`);
|
|
625
458
|
}
|
|
626
459
|
});
|
|
627
460
|
}
|
|
@@ -633,8 +466,8 @@ runMain(defineCommand({
|
|
|
633
466
|
version: VERSION
|
|
634
467
|
},
|
|
635
468
|
subCommands: {
|
|
636
|
-
init,
|
|
637
469
|
add,
|
|
470
|
+
init,
|
|
638
471
|
remove
|
|
639
472
|
}
|
|
640
473
|
}));
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "centoui-cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.0
|
|
4
|
+
"version": "1.0.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Official CLI for CentoUI.",
|
|
7
7
|
"author": "Favour Emeka <favorodera@gmail.com>",
|
|
8
8
|
"license": "MIT",
|
|
9
|
+
"funding": "https://github.com/sponsors/favorodera",
|
|
9
10
|
"homepage": "https://centoui.vercel.app",
|
|
10
11
|
"repository": {
|
|
11
12
|
"type": "git",
|
|
@@ -14,6 +15,13 @@
|
|
|
14
15
|
"bugs": {
|
|
15
16
|
"url": "https://github.com/favorodera/centoui/issues"
|
|
16
17
|
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"cli",
|
|
20
|
+
"vue",
|
|
21
|
+
"ui",
|
|
22
|
+
"components",
|
|
23
|
+
"tailwindcss"
|
|
24
|
+
],
|
|
17
25
|
"publishConfig": {
|
|
18
26
|
"access": "public"
|
|
19
27
|
},
|
|
@@ -22,33 +30,34 @@
|
|
|
22
30
|
"./package.json": "./package.json"
|
|
23
31
|
},
|
|
24
32
|
"types": "./dist/types.d.mts",
|
|
25
|
-
"files": [
|
|
26
|
-
"dist"
|
|
27
|
-
],
|
|
28
33
|
"bin": {
|
|
29
34
|
"centoui": "./dist/index.mjs"
|
|
30
35
|
},
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
"
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=22.0.0",
|
|
41
|
+
"pnpm": ">=11.0.0"
|
|
37
42
|
},
|
|
38
43
|
"dependencies": {
|
|
39
|
-
"@clack/prompts": "^1.
|
|
44
|
+
"@clack/prompts": "^1.5.1",
|
|
45
|
+
"c12": "^4.0.0-beta.5",
|
|
40
46
|
"citty": "^0.2.2",
|
|
41
|
-
"fs-extra": "^11.3.
|
|
42
|
-
"nypm": "^0.6.
|
|
47
|
+
"fs-extra": "^11.3.5",
|
|
48
|
+
"nypm": "^0.6.7",
|
|
43
49
|
"pathe": "^2.0.3"
|
|
44
50
|
},
|
|
45
|
-
"
|
|
46
|
-
"
|
|
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"
|
|
47
56
|
},
|
|
48
57
|
"scripts": {
|
|
49
58
|
"build": "tsdown",
|
|
50
59
|
"dev": "tsdown --watch",
|
|
51
|
-
"test": "vitest",
|
|
60
|
+
"test": "vitest run",
|
|
52
61
|
"test:watch": "vitest watch --ui",
|
|
53
62
|
"typecheck": "tsc --noEmit",
|
|
54
63
|
"lint": "eslint . --fix"
|