centoui-cli 0.2.1 → 1.0.0-alpha.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/dist/index.d.mts +38 -16
- package/dist/index.mjs +418 -171
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,39 +1,61 @@
|
|
|
1
1
|
//#region src/types.d.ts
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* Defines
|
|
3
|
+
* Mirrors the `globals.json` registry schema.
|
|
4
|
+
* Defines properties that every CentoUI project must have.
|
|
5
5
|
*/
|
|
6
6
|
type GlobalsRegistry = {
|
|
7
7
|
/**
|
|
8
|
-
* NPM packages that
|
|
9
|
-
* Keys are package names
|
|
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
10
|
*/
|
|
11
11
|
packageDeps: Record<string, string>;
|
|
12
12
|
};
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Mirrors the `component.json` registry schema.
|
|
15
15
|
* Describes a single installable CentoUI component.
|
|
16
16
|
*/
|
|
17
17
|
type ComponentRegistry = {
|
|
18
|
-
/** Unique component
|
|
19
|
-
description?: string;
|
|
20
|
-
|
|
21
|
-
|
|
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
|
+
*/
|
|
22
36
|
packageDeps: Record<string, string>;
|
|
23
37
|
};
|
|
24
|
-
/**
|
|
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
|
+
*/
|
|
25
42
|
type Registry = {
|
|
26
|
-
/** Global
|
|
43
|
+
/** Global properties that every CentoUI project must have. */globals: GlobalsRegistry; /** All components that can be installed with `centoui add`. */
|
|
27
44
|
components: ComponentRegistry[];
|
|
28
45
|
};
|
|
29
|
-
/**
|
|
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
|
+
*/
|
|
30
50
|
type CentoUIConfig = {
|
|
31
|
-
/** CentoUI
|
|
32
|
-
componentsDir: string; /**
|
|
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. */
|
|
33
53
|
themeFilePath: string;
|
|
34
54
|
/**
|
|
35
|
-
* Maps internal icon names to Iconify IDs
|
|
36
|
-
*
|
|
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' }
|
|
37
59
|
*/
|
|
38
60
|
icons: Record<string, string>;
|
|
39
61
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -3,109 +3,181 @@ import { defineCommand, runMain } from "citty";
|
|
|
3
3
|
import { cancel, confirm, group, intro, isCancel, log, note, outro, tasks, text } from "@clack/prompts";
|
|
4
4
|
import { dirname, join } from "pathe";
|
|
5
5
|
import fsExtra from "fs-extra";
|
|
6
|
-
import { addDependency } from "nypm";
|
|
6
|
+
import { addDependency, removeDependency } from "nypm";
|
|
7
7
|
import { pathToFileURL } from "node:url";
|
|
8
8
|
//#endregion
|
|
9
9
|
//#region src/constants.ts
|
|
10
|
-
/** CentoUI current package version */
|
|
11
|
-
const VERSION = "0.
|
|
12
|
-
/** CentoUI config
|
|
10
|
+
/** CentoUI current package version, sourced directly from package.json. */
|
|
11
|
+
const VERSION = "1.0.0-alpha.0";
|
|
12
|
+
/** File name for the user-side CentoUI config (created by `centoui init`). */
|
|
13
13
|
const CONFIG_FILE_NAME = "centoui.config.ts";
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
/**
|
|
23
|
-
const
|
|
14
|
+
/**
|
|
15
|
+
* Base URL for the CentoUI core package source tree on GitHub.
|
|
16
|
+
* Every other URL in this file is derived from this one.
|
|
17
|
+
*
|
|
18
|
+
* The path ends at `src/` so that registry and component paths from the
|
|
19
|
+
* registry (e.g. `components/button/button.vue`) can be appended directly.
|
|
20
|
+
*/
|
|
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}/css/centoui.css`;
|
|
29
|
+
/**
|
|
30
|
+
* HTTP headers required when fetching raw content from the GitHub API.
|
|
31
|
+
* These ensure we get the raw file bytes, not GitHub's HTML wrapper.
|
|
32
|
+
*/
|
|
33
|
+
const GITHUB_RAW_FETCH_HEADERS = {
|
|
24
34
|
"Accept": "application/vnd.github.raw+json",
|
|
25
35
|
"X-GitHub-Api-Version": "2026-03-10"
|
|
26
36
|
};
|
|
27
37
|
//#endregion
|
|
28
38
|
//#region src/utils/package-utils.ts
|
|
29
39
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* @
|
|
40
|
+
* Installs any packages from `requiredPackages` that are missing from — or at
|
|
41
|
+
* a different version than what is listed in — the project's `package.json`.
|
|
42
|
+
*
|
|
43
|
+
* Already-satisfied packages are skipped entirely to avoid unnecessary network
|
|
44
|
+
* traffic and lockfile churn. The package manager is auto-detected from
|
|
45
|
+
* lockfiles by nypm (npm / pnpm / yarn / bun).
|
|
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.
|
|
38
53
|
*/
|
|
39
|
-
async function
|
|
40
|
-
if (Object.keys(
|
|
54
|
+
async function installMissingPackages(requiredPackages, cwd, onProgress) {
|
|
55
|
+
if (Object.keys(requiredPackages).length === 0) return "No packages to install";
|
|
41
56
|
const packageJson = await fsExtra.readJson(join(cwd, "package.json")).catch(() => ({}));
|
|
42
|
-
const
|
|
57
|
+
const alreadyInstalled = {
|
|
43
58
|
...packageJson.dependencies,
|
|
44
59
|
...packageJson.devDependencies
|
|
45
60
|
};
|
|
46
|
-
const packagesToInstall = Object.entries(
|
|
61
|
+
const packagesToInstall = Object.entries(requiredPackages).filter(([name, version]) => alreadyInstalled[name] !== version).map(([name, version]) => `${name}@${version}`);
|
|
47
62
|
if (packagesToInstall.length === 0) return "All packages already up to date";
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
try {
|
|
64
|
+
for (const [index, pkg] of packagesToInstall.entries()) {
|
|
65
|
+
onProgress?.(`[${index + 1}/${packagesToInstall.length}] ${pkg}`);
|
|
66
|
+
await addDependency(pkg, {
|
|
67
|
+
cwd,
|
|
68
|
+
silent: true
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw new Error(`[installMissingPackages] Failed to install packages: ${error}`);
|
|
54
73
|
}
|
|
55
74
|
return `Installed ${packagesToInstall.length} package(s)`;
|
|
56
75
|
}
|
|
57
76
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
77
|
+
* Removes packages that were used by a component being uninstalled, but only
|
|
78
|
+
* if those packages are not still required by other remaining components.
|
|
79
|
+
*
|
|
80
|
+
* Skips any package that appears in `packagesStillNeeded` so that shared
|
|
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.
|
|
89
|
+
*/
|
|
90
|
+
async function removeOrphanedPackages(packagesToConsider, packagesStillNeeded, cwd, onProgress) {
|
|
91
|
+
const packagesToRemove = Object.keys(packagesToConsider).filter((name) => !(name in packagesStillNeeded));
|
|
92
|
+
if (packagesToRemove.length === 0) return "No packages to remove";
|
|
93
|
+
try {
|
|
94
|
+
for (const [index, pkg] of packagesToRemove.entries()) {
|
|
95
|
+
onProgress?.(`[${index + 1}/${packagesToRemove.length}] ${pkg}`);
|
|
96
|
+
await removeDependency(pkg, {
|
|
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)`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Validates that a value is a non-empty string usable as a file-system path.
|
|
108
|
+
*
|
|
109
|
+
* Intended as a `validate` callback for `@clack/prompts` text inputs.
|
|
110
|
+
*
|
|
111
|
+
* @param value - The raw value from the prompt.
|
|
112
|
+
* @returns An error message string if invalid, or `undefined` if valid.
|
|
60
113
|
*/
|
|
61
|
-
function
|
|
62
|
-
if (typeof
|
|
63
|
-
if (
|
|
114
|
+
function validateNonEmptyPath(value) {
|
|
115
|
+
if (typeof value !== "string") return "Expected a string path";
|
|
116
|
+
if (value.trim().length === 0) return "Path cannot be empty";
|
|
64
117
|
}
|
|
65
118
|
//#endregion
|
|
66
119
|
//#region src/utils/file-system-utils.ts
|
|
67
120
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
121
|
+
* Converts a registry-relative file path into the absolute destination path
|
|
122
|
+
* inside the user's project.
|
|
70
123
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
124
|
+
* Registry file paths always begin with `components/` (e.g.
|
|
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"`.
|
|
73
129
|
*
|
|
74
|
-
* @param
|
|
75
|
-
*
|
|
76
|
-
* @param
|
|
77
|
-
* @
|
|
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'
|
|
78
140
|
*/
|
|
79
|
-
function
|
|
80
|
-
const
|
|
81
|
-
return join(cwd, config.componentsDir,
|
|
141
|
+
function mapRegistryPathToProjectDest(registryFilePath, config, cwd) {
|
|
142
|
+
const pathWithoutRegistryPrefix = registryFilePath.replace(/^components\//, "");
|
|
143
|
+
return join(cwd, config.componentsDir, pathWithoutRegistryPrefix);
|
|
82
144
|
}
|
|
83
145
|
/**
|
|
84
|
-
*
|
|
146
|
+
* Writes `content` to `filePath`, creating every missing parent directory first.
|
|
147
|
+
*
|
|
148
|
+
* This is the standard way to write any file in the CLI — it ensures the
|
|
149
|
+
* target directory tree exists so callers don't have to `mkdir` manually.
|
|
85
150
|
*
|
|
86
|
-
* @param
|
|
87
|
-
* @param content -
|
|
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.
|
|
88
154
|
*/
|
|
89
|
-
async function
|
|
155
|
+
async function writeFileWithDirs(filePath, content) {
|
|
90
156
|
try {
|
|
91
|
-
await fsExtra.mkdir(dirname(
|
|
92
|
-
await fsExtra.writeFile(
|
|
157
|
+
await fsExtra.mkdir(dirname(filePath), { recursive: true });
|
|
158
|
+
await fsExtra.writeFile(filePath, content, "utf8");
|
|
93
159
|
} catch (error) {
|
|
94
|
-
throw new Error(`Failed to write
|
|
160
|
+
throw new Error(`[writeFileWithDirs] Failed to write "${filePath}": ${error}`);
|
|
95
161
|
}
|
|
96
162
|
}
|
|
97
163
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
164
|
+
* Prompts the user to confirm before overwriting an existing path.
|
|
165
|
+
*
|
|
166
|
+
* If `path` does not yet exist, returns `true` immediately without showing
|
|
167
|
+
* any prompt — nothing to overwrite, safe to proceed.
|
|
100
168
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
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.
|
|
103
175
|
*/
|
|
104
|
-
async function
|
|
176
|
+
async function confirmOverwriteIfExists(label, path) {
|
|
105
177
|
if (!await fsExtra.pathExists(path)) return true;
|
|
106
|
-
const answer = await confirm({ message:
|
|
178
|
+
const answer = await confirm({ message: `"${label}" already exists. Overwrite?` });
|
|
107
179
|
if (isCancel(answer)) {
|
|
108
|
-
cancel(
|
|
180
|
+
cancel("Operation cancelled.");
|
|
109
181
|
process.exit(0);
|
|
110
182
|
}
|
|
111
183
|
return answer;
|
|
@@ -113,28 +185,37 @@ async function promptOverwrite(label, path) {
|
|
|
113
185
|
//#endregion
|
|
114
186
|
//#region src/utils/config-utils.ts
|
|
115
187
|
/**
|
|
116
|
-
* Loads the user's CentoUI configuration.
|
|
188
|
+
* Loads and returns the user's CentoUI configuration from `centoui.config.ts`.
|
|
189
|
+
*
|
|
190
|
+
* Uses a dynamic `import()` via a `file://` URL so that TypeScript config files
|
|
191
|
+
* compiled by the user's build tooling are resolved correctly at runtime.
|
|
117
192
|
*
|
|
118
|
-
* @param cwd -
|
|
119
|
-
* @returns The
|
|
120
|
-
* @throws If
|
|
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.
|
|
121
197
|
*/
|
|
122
|
-
async function
|
|
198
|
+
async function loadCentoUIConfig(cwd) {
|
|
199
|
+
const configFilePath = join(cwd, CONFIG_FILE_NAME);
|
|
123
200
|
try {
|
|
124
|
-
|
|
125
|
-
if (!await fsExtra.pathExists(configFilePath)) throw `No ${CONFIG_FILE_NAME} found. Run \`centoui init\` first.`;
|
|
201
|
+
if (!await fsExtra.pathExists(configFilePath)) throw new Error(`[loadCentoUIConfig] "${CONFIG_FILE_NAME}" not found in "${cwd}". Run \`centoui init\` first.`);
|
|
126
202
|
return (await import(pathToFileURL(configFilePath).href)).default;
|
|
127
203
|
} catch (error) {
|
|
128
|
-
throw new Error(`Failed to
|
|
204
|
+
throw new Error(`[loadCentoUIConfig] Failed to import "${configFilePath}": ${error}`);
|
|
129
205
|
}
|
|
130
206
|
}
|
|
131
207
|
/**
|
|
132
|
-
* Generates the
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
208
|
+
* Generates the content of a default `centoui.config.ts` file.
|
|
209
|
+
*
|
|
210
|
+
* The generated file uses `defineConfig` so IDEs can provide type-checking
|
|
211
|
+
* and autocompletion on the config object. It pre-fills common icon mappings
|
|
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.
|
|
136
217
|
*/
|
|
137
|
-
function
|
|
218
|
+
function buildDefaultConfigFileContent(themeFilePath, componentsDir) {
|
|
138
219
|
return `import { defineConfig } from 'centoui'
|
|
139
220
|
|
|
140
221
|
export default defineConfig({
|
|
@@ -151,64 +232,111 @@ export default defineConfig({
|
|
|
151
232
|
}
|
|
152
233
|
//#endregion
|
|
153
234
|
//#region src/utils/registry-utils.ts
|
|
154
|
-
|
|
235
|
+
/** In-process cache so the registry is only fetched once per CLI invocation. */
|
|
236
|
+
let cachedRegistry = null;
|
|
155
237
|
/**
|
|
156
|
-
* Fetches the complete component registry
|
|
238
|
+
* Fetches the complete component registry (`index.json`) from GitHub, caching
|
|
239
|
+
* the result in memory for the lifetime of the process.
|
|
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.
|
|
157
243
|
*
|
|
158
|
-
* @returns The
|
|
159
|
-
* @throws If the network request fails or returns a non-
|
|
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.
|
|
160
246
|
*/
|
|
161
|
-
async function
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
247
|
+
async function fetchFullRegistry() {
|
|
248
|
+
if (cachedRegistry) return cachedRegistry;
|
|
249
|
+
let response;
|
|
250
|
+
try {
|
|
251
|
+
response = await fetch(REGISTRY_INDEX_URL, { headers: GITHUB_RAW_FETCH_HEADERS });
|
|
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;
|
|
169
258
|
}
|
|
170
259
|
/**
|
|
171
|
-
* Recursively resolves a component and
|
|
260
|
+
* Recursively resolves a component and every one of its transitive
|
|
261
|
+
* `componentDeps` into a flat map of `componentName → registry entry`.
|
|
262
|
+
*
|
|
263
|
+
* The traversal is depth-first and guards against circular dependencies via
|
|
264
|
+
* the `visited` set, so each component appears in the result exactly once.
|
|
172
265
|
*
|
|
173
|
-
* @param
|
|
174
|
-
* @param registry -
|
|
175
|
-
* @param
|
|
176
|
-
*
|
|
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.
|
|
177
272
|
*/
|
|
178
|
-
function
|
|
273
|
+
function resolveComponentWithDependencies(componentName, registry, visited = /* @__PURE__ */ new Set()) {
|
|
179
274
|
const result = /* @__PURE__ */ new Map();
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
const entry = registry.components.find((component) => component.name ===
|
|
183
|
-
if (!entry) throw new Error(`Component "${
|
|
184
|
-
result.set(
|
|
185
|
-
for (const
|
|
275
|
+
if (visited.has(componentName)) return result;
|
|
276
|
+
visited.add(componentName);
|
|
277
|
+
const entry = registry.components.find((component) => component.name === componentName);
|
|
278
|
+
if (!entry) throw new Error(`[resolveComponentWithDependencies] Component "${componentName}" not found in registry.`);
|
|
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);
|
|
186
281
|
return result;
|
|
187
282
|
}
|
|
188
283
|
/**
|
|
189
|
-
* Fetches the raw source
|
|
284
|
+
* Fetches the raw source content of a file referenced in a component's
|
|
285
|
+
* registry entry.
|
|
190
286
|
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
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.
|
|
193
295
|
*/
|
|
194
|
-
async function
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
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}"`);
|
|
198
305
|
return response.text();
|
|
199
306
|
}
|
|
200
307
|
/**
|
|
201
|
-
* Fetches the theme file from
|
|
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.
|
|
202
312
|
*
|
|
203
|
-
* @returns
|
|
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.
|
|
204
315
|
*/
|
|
205
|
-
async function
|
|
206
|
-
|
|
207
|
-
|
|
316
|
+
async function fetchThemeCSSContent() {
|
|
317
|
+
let response;
|
|
318
|
+
try {
|
|
319
|
+
response = await fetch(THEME_CSS_URL, { headers: GITHUB_RAW_FETCH_HEADERS });
|
|
320
|
+
} catch (error) {
|
|
321
|
+
throw new Error(`[fetchThemeCSSContent] Network request for theme CSS failed: ${error}`);
|
|
322
|
+
}
|
|
323
|
+
if (!response.ok) throw new Error(`[fetchThemeCSSContent] Server returned ${response.status} ${response.statusText} (URL: ${THEME_CSS_URL})`);
|
|
208
324
|
return response.text();
|
|
209
325
|
}
|
|
210
326
|
//#endregion
|
|
211
327
|
//#region src/commands/init.ts
|
|
328
|
+
/**
|
|
329
|
+
* Command: `centoui init`
|
|
330
|
+
*
|
|
331
|
+
* 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.
|
|
339
|
+
*/
|
|
212
340
|
function init() {
|
|
213
341
|
return defineCommand({
|
|
214
342
|
meta: {
|
|
@@ -223,12 +351,12 @@ function init() {
|
|
|
223
351
|
componentDir: () => text({
|
|
224
352
|
message: "Directory to store components",
|
|
225
353
|
initialValue: "src/components/centoui",
|
|
226
|
-
validate:
|
|
354
|
+
validate: validateNonEmptyPath
|
|
227
355
|
}),
|
|
228
356
|
themeFilePath: () => text({
|
|
229
|
-
message: "
|
|
357
|
+
message: "Path for the theme CSS file",
|
|
230
358
|
initialValue: "src/assets/css/centoui.css",
|
|
231
|
-
validate:
|
|
359
|
+
validate: validateNonEmptyPath
|
|
232
360
|
})
|
|
233
361
|
}, { onCancel: () => {
|
|
234
362
|
cancel("Initialization cancelled.");
|
|
@@ -237,32 +365,32 @@ function init() {
|
|
|
237
365
|
const configPath = join(cwd, CONFIG_FILE_NAME);
|
|
238
366
|
const themePath = join(cwd, directories.themeFilePath);
|
|
239
367
|
const componentsPath = join(cwd, directories.componentDir);
|
|
240
|
-
const shouldWriteConfig = await
|
|
241
|
-
const shouldWriteTheme = await
|
|
242
|
-
const
|
|
368
|
+
const shouldWriteConfig = await confirmOverwriteIfExists(CONFIG_FILE_NAME, configPath);
|
|
369
|
+
const shouldWriteTheme = await confirmOverwriteIfExists(directories.themeFilePath, themePath);
|
|
370
|
+
const shouldWriteComponentsDir = await confirmOverwriteIfExists(directories.componentDir, componentsPath);
|
|
243
371
|
let registry;
|
|
244
372
|
await tasks([
|
|
245
373
|
{
|
|
246
374
|
title: `Writing ${CONFIG_FILE_NAME}`,
|
|
247
375
|
task: async () => {
|
|
248
|
-
if (!shouldWriteConfig) return `Skipped
|
|
249
|
-
await fsExtra.outputFile(configPath,
|
|
376
|
+
if (!shouldWriteConfig) return `Skipped — "${CONFIG_FILE_NAME}" already exists`;
|
|
377
|
+
await fsExtra.outputFile(configPath, buildDefaultConfigFileContent(directories.themeFilePath, directories.componentDir), "utf-8");
|
|
250
378
|
return `${CONFIG_FILE_NAME} written`;
|
|
251
379
|
}
|
|
252
380
|
},
|
|
253
381
|
{
|
|
254
382
|
title: "Fetching theme CSS",
|
|
255
383
|
task: async () => {
|
|
256
|
-
if (!shouldWriteTheme) return
|
|
257
|
-
const
|
|
258
|
-
await fsExtra.outputFile(themePath,
|
|
384
|
+
if (!shouldWriteTheme) return `Skipped — "${directories.themeFilePath}" already exists`;
|
|
385
|
+
const themeContent = await fetchThemeCSSContent();
|
|
386
|
+
await fsExtra.outputFile(themePath, themeContent, "utf-8");
|
|
259
387
|
return `${directories.themeFilePath} written`;
|
|
260
388
|
}
|
|
261
389
|
},
|
|
262
390
|
{
|
|
263
391
|
title: "Preparing components directory",
|
|
264
392
|
task: async () => {
|
|
265
|
-
if (!
|
|
393
|
+
if (!shouldWriteComponentsDir) return `Skipped — "${directories.componentDir}" already exists`;
|
|
266
394
|
await fsExtra.emptyDir(componentsPath);
|
|
267
395
|
return `${directories.componentDir} ready`;
|
|
268
396
|
}
|
|
@@ -270,15 +398,13 @@ function init() {
|
|
|
270
398
|
{
|
|
271
399
|
title: "Fetching registry",
|
|
272
400
|
task: async () => {
|
|
273
|
-
registry = await
|
|
274
|
-
return "Registry
|
|
401
|
+
registry = await fetchFullRegistry();
|
|
402
|
+
return "Registry loaded";
|
|
275
403
|
}
|
|
276
404
|
},
|
|
277
405
|
{
|
|
278
406
|
title: "Installing global dependencies",
|
|
279
|
-
task: async (message) =>
|
|
280
|
-
return installPackages(registry.globals.packageDeps, cwd, message);
|
|
281
|
-
}
|
|
407
|
+
task: async (message) => installMissingPackages(registry.globals.packageDeps, cwd, message)
|
|
282
408
|
}
|
|
283
409
|
]);
|
|
284
410
|
note([
|
|
@@ -286,11 +412,11 @@ function init() {
|
|
|
286
412
|
`Theme > ${themePath}`,
|
|
287
413
|
`Components > ${componentsPath}`,
|
|
288
414
|
"",
|
|
289
|
-
"Run 'centoui add button' to install your first component"
|
|
290
|
-
].join("\n"), "CentoUI initialized");
|
|
291
|
-
outro("All
|
|
415
|
+
"Run 'centoui add button' to install your first component."
|
|
416
|
+
].filter(Boolean).join("\n"), "CentoUI initialized");
|
|
417
|
+
outro("All set!");
|
|
292
418
|
} catch (error) {
|
|
293
|
-
log.error(`
|
|
419
|
+
log.error(`Initialization failed: ${error}`);
|
|
294
420
|
process.exit(1);
|
|
295
421
|
}
|
|
296
422
|
}
|
|
@@ -299,23 +425,71 @@ function init() {
|
|
|
299
425
|
//#endregion
|
|
300
426
|
//#region src/utils/components-utils.ts
|
|
301
427
|
/**
|
|
302
|
-
*
|
|
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.
|
|
303
455
|
*
|
|
304
|
-
* @param
|
|
305
|
-
* @param config - CentoUI configuration
|
|
306
|
-
* @param cwd -
|
|
307
|
-
* @returns The absolute path to the component directory
|
|
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.
|
|
308
459
|
*/
|
|
309
|
-
function
|
|
310
|
-
return join(cwd, config.componentsDir,
|
|
460
|
+
function resolveComponentInstallDir(componentName, config, cwd) {
|
|
461
|
+
return join(cwd, config.componentsDir, componentName);
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Checks whether a component is currently installed by testing for the
|
|
465
|
+
* existence of its installation directory.
|
|
466
|
+
*
|
|
467
|
+
* @param componentName - The component name in kebab-case (e.g. `"button"`).
|
|
468
|
+
* @param config - The loaded CentoUI project configuration.
|
|
469
|
+
* @param cwd - Absolute path to the project root.
|
|
470
|
+
* @returns `true` if the component directory exists, `false` otherwise.
|
|
471
|
+
*/
|
|
472
|
+
async function checkIsComponentInstalled(componentName, config, cwd) {
|
|
473
|
+
const componentInstallDir = resolveComponentInstallDir(componentName, config, cwd);
|
|
474
|
+
try {
|
|
475
|
+
return fsExtra.pathExists(componentInstallDir);
|
|
476
|
+
} catch (error) {
|
|
477
|
+
throw new Error(`[checkIsComponentInstalled] Failed to check if component "${componentName}" is installed: ${error}`);
|
|
478
|
+
}
|
|
311
479
|
}
|
|
312
480
|
//#endregion
|
|
313
481
|
//#region src/commands/add.ts
|
|
314
482
|
/**
|
|
315
|
-
* Command: add
|
|
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.
|
|
316
487
|
*
|
|
317
|
-
*
|
|
318
|
-
* the
|
|
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.
|
|
319
493
|
*/
|
|
320
494
|
function add() {
|
|
321
495
|
return defineCommand({
|
|
@@ -327,50 +501,46 @@ function add() {
|
|
|
327
501
|
async run({ args }) {
|
|
328
502
|
try {
|
|
329
503
|
const cwd = process.cwd();
|
|
330
|
-
const
|
|
504
|
+
const requestedNames = args._;
|
|
331
505
|
intro("CentoUI — Add components");
|
|
332
|
-
if (
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
const config = await loadUserConfig(cwd);
|
|
337
|
-
const registry = await fetchRegistry();
|
|
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();
|
|
338
509
|
const allComponents = /* @__PURE__ */ new Map();
|
|
339
|
-
for (const name of
|
|
340
|
-
const tree =
|
|
510
|
+
for (const name of requestedNames) try {
|
|
511
|
+
const tree = resolveComponentWithDependencies(name, registry);
|
|
341
512
|
for (const [depName, depEntry] of tree) allComponents.set(depName, depEntry);
|
|
342
513
|
} catch (error) {
|
|
343
|
-
|
|
344
|
-
process.exit(1);
|
|
514
|
+
throw new Error(`Failed to resolve "${name}": ${error}`);
|
|
345
515
|
}
|
|
346
|
-
const
|
|
516
|
+
const writeDecisions = /* @__PURE__ */ new Map();
|
|
347
517
|
for (const [name] of allComponents) {
|
|
348
|
-
const shouldWrite = await
|
|
349
|
-
|
|
518
|
+
const shouldWrite = await confirmOverwriteIfExists(name, resolveComponentInstallDir(name, config, cwd));
|
|
519
|
+
writeDecisions.set(name, shouldWrite);
|
|
350
520
|
}
|
|
351
|
-
const
|
|
352
|
-
for (const [name, entry] of allComponents) if (
|
|
353
|
-
const
|
|
354
|
-
await tasks([...
|
|
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]) => ({
|
|
355
525
|
title: `Installing ${name}`,
|
|
356
526
|
task: async () => {
|
|
357
|
-
for (const
|
|
358
|
-
const content = await
|
|
359
|
-
await
|
|
527
|
+
for (const registryFilePath of entry.files) {
|
|
528
|
+
const content = await fetchRegistryFileContent(registryFilePath);
|
|
529
|
+
await writeFileWithDirs(mapRegistryPathToProjectDest(registryFilePath, config, cwd), content);
|
|
360
530
|
}
|
|
361
|
-
return `${name} installed (${entry.files.length} file
|
|
531
|
+
return `${name} installed (${entry.files.length} file(s))`;
|
|
362
532
|
}
|
|
363
533
|
})), {
|
|
364
534
|
title: "Installing packages",
|
|
365
|
-
task: async (message) =>
|
|
535
|
+
task: async (message) => installMissingPackages(packageDepsToInstall, cwd, message)
|
|
366
536
|
}]);
|
|
367
|
-
const
|
|
537
|
+
const skippedNames = Array.from(writeDecisions.entries()).filter(([, shouldWrite]) => !shouldWrite).map(([name]) => name);
|
|
368
538
|
note([
|
|
369
|
-
`Installed > ${
|
|
370
|
-
|
|
539
|
+
`Installed > ${approvedComponents.map(([name]) => name).join(", ") || "none"}`,
|
|
540
|
+
skippedNames.length > 0 ? `Skipped > ${skippedNames.join(", ")}` : "",
|
|
371
541
|
"",
|
|
372
|
-
"Import components from your components directory to use them"
|
|
373
|
-
].filter(Boolean).join("\n"), "
|
|
542
|
+
"Import components from your components directory to use them."
|
|
543
|
+
].filter(Boolean).join("\n"), "Component(s) added");
|
|
374
544
|
outro("All set!");
|
|
375
545
|
} catch (error) {
|
|
376
546
|
log.error(`Failed to add component(s): ${error}`);
|
|
@@ -380,6 +550,82 @@ function add() {
|
|
|
380
550
|
});
|
|
381
551
|
}
|
|
382
552
|
//#endregion
|
|
553
|
+
//#region src/commands/remove.ts
|
|
554
|
+
/**
|
|
555
|
+
* Command: `centoui remove <component>`
|
|
556
|
+
*
|
|
557
|
+
* 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.
|
|
567
|
+
*/
|
|
568
|
+
function remove() {
|
|
569
|
+
return defineCommand({
|
|
570
|
+
meta: {
|
|
571
|
+
name: "remove",
|
|
572
|
+
description: "Remove an installed component from your project"
|
|
573
|
+
},
|
|
574
|
+
args: { component: {
|
|
575
|
+
type: "positional",
|
|
576
|
+
required: true,
|
|
577
|
+
description: "Name of the component to remove (e.g. \"button\")"
|
|
578
|
+
} },
|
|
579
|
+
async run({ args }) {
|
|
580
|
+
try {
|
|
581
|
+
const cwd = process.cwd();
|
|
582
|
+
const componentName = args.component;
|
|
583
|
+
intro("CentoUI — Remove component");
|
|
584
|
+
const config = await loadCentoUIConfig(cwd);
|
|
585
|
+
if (!await checkIsComponentInstalled(componentName, config, cwd)) throw new Error(`Component "${componentName}" is not installed.`);
|
|
586
|
+
const componentInstallDir = resolveComponentInstallDir(componentName, config, cwd);
|
|
587
|
+
const registry = await fetchFullRegistry();
|
|
588
|
+
const targetEntry = registry.components.find((component) => component.name === componentName);
|
|
589
|
+
if (!targetEntry) throw new Error(`"${componentName}" was not found in the registry. Aborting to avoid a partial removal.`);
|
|
590
|
+
const remainingNames = (await listInstalledComponentNames(config, cwd)).filter((name) => name !== componentName);
|
|
591
|
+
const dependents = /* @__PURE__ */ new Set();
|
|
592
|
+
const packagesStillNeeded = {};
|
|
593
|
+
for (const name of remainingNames) {
|
|
594
|
+
const entry = registry.components.find((component) => component.name === name);
|
|
595
|
+
if (!entry) continue;
|
|
596
|
+
if (entry.componentDeps.includes(componentName)) dependents.add(name);
|
|
597
|
+
Object.assign(packagesStillNeeded, entry.packageDeps);
|
|
598
|
+
}
|
|
599
|
+
if (dependents.size > 0) {
|
|
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);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
//#endregion
|
|
383
629
|
//#region src/index.ts
|
|
384
630
|
runMain(defineCommand({
|
|
385
631
|
meta: {
|
|
@@ -388,7 +634,8 @@ runMain(defineCommand({
|
|
|
388
634
|
},
|
|
389
635
|
subCommands: {
|
|
390
636
|
init,
|
|
391
|
-
add
|
|
637
|
+
add,
|
|
638
|
+
remove
|
|
392
639
|
}
|
|
393
640
|
}));
|
|
394
641
|
//#endregion
|