ui-thing 0.2.9 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.claude/settings.local.json +5 -0
  2. package/.github/workflows/main.yml +7 -4
  3. package/.github/workflows/test.yml +7 -4
  4. package/.prettierrc +1 -1
  5. package/CHANGELOG.md +52 -0
  6. package/README.md +208 -21
  7. package/bun.lock +171 -134
  8. package/dist/index.js +28 -25
  9. package/dist/index.js.map +1 -1
  10. package/package.json +15 -14
  11. package/src/commands/add.ts +41 -12
  12. package/src/commands/block.ts +5 -3
  13. package/src/commands/list.ts +48 -0
  14. package/src/commands/prose.ts +7 -3
  15. package/src/commands/remove.ts +109 -0
  16. package/src/commands/theme.ts +5 -14
  17. package/src/commands/update.ts +81 -0
  18. package/src/index.ts +6 -0
  19. package/src/types.ts +12 -0
  20. package/src/utils/config.ts +9 -16
  21. package/src/utils/constants.ts +1 -0
  22. package/src/utils/fetchBlockCategories.ts +13 -11
  23. package/src/utils/fetchBlocks.ts +13 -11
  24. package/src/utils/fetchComponents.ts +13 -11
  25. package/src/utils/fetchProseComponents.ts +13 -11
  26. package/src/utils/installPackages.ts +30 -4
  27. package/src/utils/installValidator.ts +3 -3
  28. package/src/utils/logger.ts +9 -0
  29. package/src/utils/uiConfigPrompt.ts +3 -6
  30. package/tests/commands/add.test.ts +136 -0
  31. package/tests/commands/list.test.ts +111 -0
  32. package/tests/commands/remove.test.ts +151 -0
  33. package/tests/commands/update.test.ts +127 -0
  34. package/tests/utils/fetchBlockCategories.test.ts +14 -2
  35. package/tests/utils/fetchBlocks.test.ts +14 -2
  36. package/tests/utils/fetchComponents.test.ts +10 -4
  37. package/tests/utils/fetchProseComponents.test.ts +14 -2
  38. package/tests/utils/installPackages.test.ts +36 -3
@@ -1,4 +1,5 @@
1
1
  import axios from "axios";
2
+ import { consola } from "consola";
2
3
  import dotenv from "dotenv";
3
4
  import ora from "ora";
4
5
 
@@ -6,16 +7,17 @@ import { BlockComponent } from "../types";
6
7
 
7
8
  dotenv.config();
8
9
 
9
- /**
10
- * Fetch block components from UI Thing API.
11
- */
12
- export const fetchBlocks = async () => {
10
+ export const fetchBlocks = async (): Promise<BlockComponent[]> => {
13
11
  const spinner = ora("Fetching blocks...").start();
14
-
15
- const { data } = await axios.get<BlockComponent[]>(
16
- process.env.BLOCKS_API || "https://uithing.com/api/blocks"
17
- );
18
-
19
- spinner.succeed("Blocks fetched.");
20
- return data;
12
+ try {
13
+ const { data } = await axios.get<BlockComponent[]>(
14
+ process.env.BLOCKS_API || "https://uithing.com/api/blocks"
15
+ );
16
+ spinner.succeed("Blocks fetched.");
17
+ return data;
18
+ } catch {
19
+ spinner.fail("Failed to fetch blocks.");
20
+ consola.error("Could not reach the UI Thing API. Check your network connection.");
21
+ process.exit(1);
22
+ }
21
23
  };
@@ -1,4 +1,5 @@
1
1
  import axios from "axios";
2
+ import { consola } from "consola";
2
3
  import dotenv from "dotenv";
3
4
  import ora from "ora";
4
5
 
@@ -6,16 +7,17 @@ import { Component } from "../types";
6
7
 
7
8
  dotenv.config();
8
9
 
9
- /**
10
- * Function used to fetch components from the API.
11
- */
12
- export const fetchComponents = async () => {
10
+ export const fetchComponents = async (): Promise<Component[]> => {
13
11
  const spinner = ora("Fetching components...").start();
14
-
15
- const { data } = await axios.get<Component[]>(
16
- process.env.COMPONENTS_API || "https://uithing.com/api/components"
17
- );
18
- spinner.succeed("Components fetched.");
19
-
20
- return data;
12
+ try {
13
+ const { data } = await axios.get<Component[]>(
14
+ process.env.COMPONENTS_API || "https://uithing.com/api/components"
15
+ );
16
+ spinner.succeed("Components fetched.");
17
+ return data;
18
+ } catch {
19
+ spinner.fail("Failed to fetch components.");
20
+ consola.error("Could not reach the UI Thing API. Check your network connection.");
21
+ process.exit(1);
22
+ }
21
23
  };
@@ -1,4 +1,5 @@
1
1
  import axios from "axios";
2
+ import { consola } from "consola";
2
3
  import dotenv from "dotenv";
3
4
  import ora from "ora";
4
5
 
@@ -6,16 +7,17 @@ import { ProseComponent } from "../types";
6
7
 
7
8
  dotenv.config();
8
9
 
9
- /**
10
- * Fetch prose components from UI Thing API.
11
- */
12
- export const fetchProseComponents = async () => {
10
+ export const fetchProseComponents = async (): Promise<ProseComponent[]> => {
13
11
  const spinner = ora("Fetching prose components...").start();
14
-
15
- const { data } = await axios.get<ProseComponent[]>(
16
- process.env.PROSE_COMPONENTS_API || "https://uithing.com/api/prose"
17
- );
18
-
19
- spinner.succeed("Prose components fetched.");
20
- return data;
12
+ try {
13
+ const { data } = await axios.get<ProseComponent[]>(
14
+ process.env.PROSE_COMPONENTS_API || "https://uithing.com/api/prose"
15
+ );
16
+ spinner.succeed("Prose components fetched.");
17
+ return data;
18
+ } catch {
19
+ spinner.fail("Failed to fetch prose components.");
20
+ consola.error("Could not reach the UI Thing API. Check your network connection.");
21
+ process.exit(1);
22
+ }
21
23
  };
@@ -2,6 +2,26 @@ import { execa } from "execa";
2
2
  import _ from "lodash";
3
3
  import ora from "ora";
4
4
 
5
+ const PM_ADD_CMD: Record<string, string> = {
6
+ yarn: "add",
7
+ bun: "add",
8
+ pnpm: "add",
9
+ npm: "install",
10
+ deno: "add",
11
+ };
12
+
13
+ const PM_DEV_FLAG: Record<string, string> = {
14
+ yarn: "-D",
15
+ bun: "-D",
16
+ pnpm: "-D",
17
+ npm: "-D",
18
+ deno: "--dev",
19
+ };
20
+
21
+ // Deno 2.x requires `npm:` prefix for npm packages
22
+ const formatPkgs = (pm: string, pkgs: string[]) =>
23
+ pm === "deno" ? pkgs.map((p) => (p.startsWith("npm:") ? p : `npm:${p}`)) : pkgs;
24
+
5
25
  export const installPackages = async (
6
26
  packageManager: string,
7
27
  deps?: string[] | string,
@@ -14,18 +34,24 @@ export const installPackages = async (
14
34
  devDeps = [devDeps];
15
35
  }
16
36
 
37
+ const addCmd = PM_ADD_CMD[packageManager] ?? "install";
38
+ const devFlag = PM_DEV_FLAG[packageManager] ?? "-D";
39
+
17
40
  const depsSpinner = ora("Installing dependencies...").start();
18
41
  if (!_.isUndefined(deps) && !_.isEmpty(deps)) {
19
- await execa(packageManager, [packageManager === "yarn" ? "add" : "install", ...deps]);
42
+ await execa(packageManager, [addCmd, ...formatPkgs(packageManager, deps)]);
20
43
  }
21
44
  depsSpinner.text = "Installing dev dependencies...";
22
45
  if (!_.isUndefined(devDeps) && !_.isEmpty(devDeps)) {
23
- await execa(packageManager, [packageManager === "yarn" ? "add" : "install", "-D", ...devDeps]);
46
+ await execa(packageManager, [addCmd, devFlag, ...formatPkgs(packageManager, devDeps)]);
24
47
  }
25
48
 
26
- // we should check to see if there is a postinstall script and run it
27
49
  depsSpinner.text = "Running nuxt prepare...";
28
- await execa`npx -y nuxt prepare`;
50
+ if (packageManager === "deno") {
51
+ await execa("deno", ["run", "-A", "npm:nuxt", "prepare"]);
52
+ } else {
53
+ await execa`npx -y nuxt prepare`;
54
+ }
29
55
 
30
56
  depsSpinner.succeed("Installed dependencies!");
31
57
  };
@@ -1,9 +1,9 @@
1
1
  import prompts from "prompts";
2
2
 
3
3
  import { installPackages } from "./installPackages";
4
+ import { logger } from "./logger";
4
5
 
5
6
  export const installValidator = async (packageManager: string) => {
6
- // Depending on the selected validator, install the corresponding packages
7
7
  const validatorPackages = {
8
8
  yup: ["yup", "@vee-validate/yup"],
9
9
  zod: ["zod", "@vee-validate/zod"],
@@ -23,10 +23,10 @@ export const installValidator = async (packageManager: string) => {
23
23
  ],
24
24
  });
25
25
  if (!validator) {
26
- console.log("No validator package selected");
26
+ logger.warn("No validator package selected");
27
27
  return;
28
28
  }
29
- console.log(`Selected ${validator} as the validator package`);
29
+ logger.info(`Selected ${validator} as the validator package`);
30
30
 
31
31
  if (validatorPackages[validator]) {
32
32
  await installPackages(packageManager, validatorPackages[validator]);
@@ -0,0 +1,9 @@
1
+ import { consola } from "consola";
2
+
3
+ export const logger = {
4
+ info: (msg: string, ...args: unknown[]) => consola.info(msg, ...args),
5
+ error: (msg: string, ...args: unknown[]) => consola.error(msg, ...args),
6
+ warn: (msg: string, ...args: unknown[]) => consola.warn(msg, ...args),
7
+ success: (msg: string, ...args: unknown[]) => consola.success(msg, ...args),
8
+ log: (msg: string, ...args: unknown[]) => consola.log(msg, ...args),
9
+ };
@@ -1,12 +1,9 @@
1
1
  /* eslint-disable @typescript-eslint/no-unused-vars */
2
- import kleur from "kleur";
3
2
  import prompts from "prompts";
4
3
 
5
4
  import { CSS_THEME_OPTIONS, PACKAGE_MANAGER_CHOICES } from "./constants";
5
+ import { logger } from "./logger";
6
6
 
7
- /**
8
- * Prompts the user for UI configuration values.
9
- */
10
7
  export const initPrompts = async (nuxtVersion: number) => {
11
8
  const response = await prompts([
12
9
  {
@@ -67,8 +64,8 @@ export const initPrompts = async (nuxtVersion: number) => {
67
64
  ]);
68
65
 
69
66
  if (!response || Object.keys(response).length < 9) {
70
- console.log(kleur.red(kleur.bold("Incomplete configuration submitted. Exiting...")));
71
- return process.exit(0);
67
+ logger.error("Incomplete configuration submitted. Exiting...");
68
+ return process.exit(1);
72
69
  }
73
70
  return response;
74
71
  };
@@ -0,0 +1,136 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import { runAddCommand } from "../../src/commands/add";
4
+ import { Component } from "../../src/types";
5
+
6
+ vi.mock("../../src/utils/fetchComponents");
7
+ vi.mock("../../src/utils/config");
8
+ vi.mock("../../src/utils/compareUIConfig");
9
+ vi.mock("../../src/utils/detectNuxtVersion");
10
+ vi.mock("../../src/utils/writeFile");
11
+ vi.mock("../../src/utils/installPackages");
12
+ vi.mock("../../src/utils/fileExists");
13
+ vi.mock("../../src/utils/logger");
14
+ vi.mock("../../src/utils/printFancyBoxMessage");
15
+ vi.mock("../../src/utils/promptForComponents");
16
+ vi.mock("../../src/utils/installValidator");
17
+ vi.mock("prompts");
18
+ vi.mock("c12/update");
19
+
20
+ const mockComponent: Component = {
21
+ name: "Popover",
22
+ value: "popover",
23
+ files: [{ fileName: "Popover.vue", dirPath: "app/components/Ui", fileContent: "<div />" }],
24
+ utils: [],
25
+ composables: [],
26
+ plugins: [],
27
+ deps: [],
28
+ devDeps: [],
29
+ nuxtModules: [],
30
+ };
31
+
32
+ describe("commands/add --skip-config", () => {
33
+ beforeEach(() => {
34
+ vi.clearAllMocks();
35
+ });
36
+
37
+ afterEach(() => {
38
+ vi.restoreAllMocks();
39
+ });
40
+
41
+ it("uses Nuxt 4 defaults when skipConfig is set and nuxt 4 is detected", async () => {
42
+ const { fetchComponents } = await import("../../src/utils/fetchComponents");
43
+ const { detectNuxtVersion } = await import("../../src/utils/detectNuxtVersion");
44
+ const { writeFile } = await import("../../src/utils/writeFile");
45
+ const { fileExists } = await import("../../src/utils/fileExists");
46
+ const prompts = await import("prompts");
47
+
48
+ vi.mocked(fetchComponents).mockResolvedValue([mockComponent]);
49
+ vi.mocked(detectNuxtVersion).mockReturnValue(4);
50
+ vi.mocked(fileExists).mockResolvedValue(false);
51
+ vi.mocked(prompts.default).mockResolvedValue({});
52
+
53
+ await runAddCommand(["popover"], { skipConfig: true, packageManager: "npm" });
54
+
55
+ expect(vi.mocked(writeFile)).toHaveBeenCalledWith(
56
+ expect.stringContaining("app/components/Ui/Popover.vue"),
57
+ "<div />"
58
+ );
59
+ });
60
+
61
+ it("uses Nuxt 3 defaults when skipConfig is set and nuxt 3 is detected", async () => {
62
+ const { fetchComponents } = await import("../../src/utils/fetchComponents");
63
+ const { detectNuxtVersion } = await import("../../src/utils/detectNuxtVersion");
64
+ const { writeFile } = await import("../../src/utils/writeFile");
65
+ const { fileExists } = await import("../../src/utils/fileExists");
66
+ const prompts = await import("prompts");
67
+
68
+ vi.mocked(fetchComponents).mockResolvedValue([mockComponent]);
69
+ vi.mocked(detectNuxtVersion).mockReturnValue(3);
70
+ vi.mocked(fileExists).mockResolvedValue(false);
71
+ vi.mocked(prompts.default).mockResolvedValue({});
72
+
73
+ await runAddCommand(["popover"], { skipConfig: true, packageManager: "npm" });
74
+
75
+ expect(vi.mocked(writeFile)).toHaveBeenCalledWith(
76
+ expect.stringContaining("components/Ui/Popover.vue"),
77
+ "<div />"
78
+ );
79
+ expect(vi.mocked(writeFile)).not.toHaveBeenCalledWith(
80
+ expect.stringContaining("app/components/Ui/Popover.vue"),
81
+ expect.any(String)
82
+ );
83
+ });
84
+
85
+ it("uses the provided --package-manager without prompting", async () => {
86
+ const { fetchComponents } = await import("../../src/utils/fetchComponents");
87
+ const { detectNuxtVersion } = await import("../../src/utils/detectNuxtVersion");
88
+ const { fileExists } = await import("../../src/utils/fileExists");
89
+ const prompts = await import("prompts");
90
+
91
+ vi.mocked(fetchComponents).mockResolvedValue([mockComponent]);
92
+ vi.mocked(detectNuxtVersion).mockReturnValue(4);
93
+ vi.mocked(fileExists).mockResolvedValue(false);
94
+
95
+ await runAddCommand(["popover"], { skipConfig: true, packageManager: "bun" });
96
+
97
+ expect(vi.mocked(prompts.default)).not.toHaveBeenCalledWith(
98
+ expect.objectContaining({ name: "packageManager" })
99
+ );
100
+ });
101
+
102
+ it("prompts for package manager when skipConfig is set but no --package-manager given", async () => {
103
+ const { fetchComponents } = await import("../../src/utils/fetchComponents");
104
+ const { detectNuxtVersion } = await import("../../src/utils/detectNuxtVersion");
105
+ const { fileExists } = await import("../../src/utils/fileExists");
106
+ const prompts = await import("prompts");
107
+
108
+ vi.mocked(fetchComponents).mockResolvedValue([mockComponent]);
109
+ vi.mocked(detectNuxtVersion).mockReturnValue(4);
110
+ vi.mocked(fileExists).mockResolvedValue(false);
111
+ vi.mocked(prompts.default).mockResolvedValue({ packageManager: "pnpm" });
112
+
113
+ await runAddCommand(["popover"], { skipConfig: true });
114
+
115
+ expect(vi.mocked(prompts.default)).toHaveBeenCalledWith(
116
+ expect.objectContaining({ name: "packageManager", type: "select" })
117
+ );
118
+ });
119
+
120
+ it("does not call getUIConfig when skipConfig is set", async () => {
121
+ const { fetchComponents } = await import("../../src/utils/fetchComponents");
122
+ const { detectNuxtVersion } = await import("../../src/utils/detectNuxtVersion");
123
+ const { getUIConfig } = await import("../../src/utils/config");
124
+ const { fileExists } = await import("../../src/utils/fileExists");
125
+ const prompts = await import("prompts");
126
+
127
+ vi.mocked(fetchComponents).mockResolvedValue([mockComponent]);
128
+ vi.mocked(detectNuxtVersion).mockReturnValue(4);
129
+ vi.mocked(fileExists).mockResolvedValue(false);
130
+ vi.mocked(prompts.default).mockResolvedValue({});
131
+
132
+ await runAddCommand(["popover"], { skipConfig: true, packageManager: "npm" });
133
+
134
+ expect(vi.mocked(getUIConfig)).not.toHaveBeenCalled();
135
+ });
136
+ });
@@ -0,0 +1,111 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import { runListCommand } from "../../src/commands/list";
4
+ import { Component } from "../../src/types";
5
+
6
+ vi.mock("../../src/utils/fetchComponents");
7
+ vi.mock("../../src/utils/config");
8
+ vi.mock("../../src/utils/fileExists");
9
+ vi.mock("../../src/utils/logger");
10
+
11
+ const mockComponents: Component[] = [
12
+ {
13
+ name: "Button",
14
+ value: "button",
15
+ files: [{ fileName: "Button.vue", dirPath: "app/components/Ui", fileContent: "" }],
16
+ utils: [],
17
+ composables: [],
18
+ plugins: [],
19
+ },
20
+ {
21
+ name: "Input",
22
+ value: "input",
23
+ files: [{ fileName: "Input.vue", dirPath: "app/components/Ui", fileContent: "" }],
24
+ utils: [],
25
+ composables: [],
26
+ plugins: [],
27
+ },
28
+ {
29
+ name: "Card",
30
+ value: "card",
31
+ files: [{ fileName: "Card/Card.vue", dirPath: "app/components/Ui", fileContent: "" }],
32
+ utils: [],
33
+ composables: [],
34
+ plugins: [],
35
+ },
36
+ ];
37
+
38
+ describe("commands/list", () => {
39
+ beforeEach(() => {
40
+ vi.clearAllMocks();
41
+ });
42
+
43
+ afterEach(() => {
44
+ vi.restoreAllMocks();
45
+ });
46
+
47
+ it("should list all components when --installed is not passed", async () => {
48
+ const { fetchComponents } = await import("../../src/utils/fetchComponents");
49
+ const { getUIConfig } = await import("../../src/utils/config");
50
+ const { logger } = await import("../../src/utils/logger");
51
+
52
+ vi.mocked(getUIConfig).mockResolvedValue({ componentsLocation: "app/components/Ui" } as any);
53
+ vi.mocked(fetchComponents).mockResolvedValue(mockComponents);
54
+
55
+ await runListCommand({ installed: false });
56
+
57
+ expect(vi.mocked(logger.log)).toHaveBeenCalledTimes(3);
58
+ expect(vi.mocked(logger.success)).toHaveBeenCalledWith("3 component(s) found.");
59
+ });
60
+
61
+ it("should filter to installed components when --installed is passed", async () => {
62
+ const { fetchComponents } = await import("../../src/utils/fetchComponents");
63
+ const { getUIConfig } = await import("../../src/utils/config");
64
+ const { fileExists } = await import("../../src/utils/fileExists");
65
+ const { logger } = await import("../../src/utils/logger");
66
+
67
+ vi.mocked(getUIConfig).mockResolvedValue({ componentsLocation: "app/components/Ui" } as any);
68
+ vi.mocked(fetchComponents).mockResolvedValue(mockComponents);
69
+ // Only Button is "installed"
70
+ vi.mocked(fileExists)
71
+ .mockResolvedValueOnce(true)
72
+ .mockResolvedValueOnce(false)
73
+ .mockResolvedValueOnce(false);
74
+
75
+ await runListCommand({ installed: true });
76
+
77
+ expect(vi.mocked(logger.log)).toHaveBeenCalledTimes(1);
78
+ expect(vi.mocked(logger.log)).toHaveBeenCalledWith("Button (button)");
79
+ expect(vi.mocked(logger.success)).toHaveBeenCalledWith("1 component(s) found.");
80
+ });
81
+
82
+ it("should show 'No components installed.' when --installed and none found", async () => {
83
+ const { fetchComponents } = await import("../../src/utils/fetchComponents");
84
+ const { getUIConfig } = await import("../../src/utils/config");
85
+ const { fileExists } = await import("../../src/utils/fileExists");
86
+ const { logger } = await import("../../src/utils/logger");
87
+
88
+ vi.mocked(getUIConfig).mockResolvedValue({ componentsLocation: "app/components/Ui" } as any);
89
+ vi.mocked(fetchComponents).mockResolvedValue(mockComponents);
90
+ vi.mocked(fileExists).mockResolvedValue(false);
91
+
92
+ await runListCommand({ installed: true });
93
+
94
+ expect(vi.mocked(logger.info)).toHaveBeenCalledWith("No components installed.");
95
+ expect(vi.mocked(logger.log)).not.toHaveBeenCalled();
96
+ });
97
+
98
+ it("should show 'No components available.' when API returns empty array", async () => {
99
+ const { fetchComponents } = await import("../../src/utils/fetchComponents");
100
+ const { getUIConfig } = await import("../../src/utils/config");
101
+ const { logger } = await import("../../src/utils/logger");
102
+
103
+ vi.mocked(getUIConfig).mockResolvedValue({ componentsLocation: "app/components/Ui" } as any);
104
+ vi.mocked(fetchComponents).mockResolvedValue([]);
105
+
106
+ await runListCommand({ installed: false });
107
+
108
+ expect(vi.mocked(logger.info)).toHaveBeenCalledWith("No components available.");
109
+ expect(vi.mocked(logger.log)).not.toHaveBeenCalled();
110
+ });
111
+ });
@@ -0,0 +1,151 @@
1
+ import fs from "node:fs/promises";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ import { runRemoveCommand } from "../../src/commands/remove";
5
+ import { Component } from "../../src/types";
6
+
7
+ vi.mock("../../src/utils/fetchComponents");
8
+ vi.mock("../../src/utils/config");
9
+ vi.mock("../../src/utils/fileExists");
10
+ vi.mock("../../src/utils/logger");
11
+ vi.mock("../../src/utils/printFancyBoxMessage");
12
+ vi.mock("prompts");
13
+ vi.mock("node:fs/promises");
14
+
15
+ const mockComponents: Component[] = [
16
+ {
17
+ name: "Button",
18
+ value: "button",
19
+ files: [{ fileName: "Button.vue", dirPath: "app/components/Ui", fileContent: "" }],
20
+ utils: [],
21
+ composables: [],
22
+ plugins: [],
23
+ },
24
+ {
25
+ name: "Input",
26
+ value: "input",
27
+ files: [{ fileName: "Input.vue", dirPath: "app/components/Ui", fileContent: "" }],
28
+ utils: [],
29
+ composables: [],
30
+ plugins: [],
31
+ },
32
+ ];
33
+
34
+ describe("commands/remove", () => {
35
+ beforeEach(() => {
36
+ vi.clearAllMocks();
37
+ });
38
+
39
+ afterEach(() => {
40
+ vi.restoreAllMocks();
41
+ });
42
+
43
+ it("should remove component files that exist", async () => {
44
+ const { fetchComponents } = await import("../../src/utils/fetchComponents");
45
+ const { getUIConfig } = await import("../../src/utils/config");
46
+ const { fileExists } = await import("../../src/utils/fileExists");
47
+ const { logger } = await import("../../src/utils/logger");
48
+ const prompts = await import("prompts");
49
+
50
+ vi.mocked(getUIConfig).mockResolvedValue({
51
+ componentsLocation: "app/components/Ui",
52
+ utilsLocation: "app/utils",
53
+ composablesLocation: "app/composables",
54
+ } as any);
55
+ vi.mocked(fetchComponents).mockResolvedValue(mockComponents);
56
+ vi.mocked(fileExists).mockResolvedValue(true);
57
+ vi.mocked(prompts.default).mockResolvedValue({ confirmed: true });
58
+ vi.mocked(fs.unlink).mockResolvedValue(undefined);
59
+
60
+ await runRemoveCommand(["button"]);
61
+
62
+ expect(vi.mocked(fs.unlink)).toHaveBeenCalledWith(expect.stringContaining("Button.vue"));
63
+ expect(vi.mocked(logger.success)).toHaveBeenCalledWith("Removed: Button.vue");
64
+ });
65
+
66
+ it("should skip files that do not exist and warn", async () => {
67
+ const { fetchComponents } = await import("../../src/utils/fetchComponents");
68
+ const { getUIConfig } = await import("../../src/utils/config");
69
+ const { fileExists } = await import("../../src/utils/fileExists");
70
+ const { logger } = await import("../../src/utils/logger");
71
+ const prompts = await import("prompts");
72
+
73
+ vi.mocked(getUIConfig).mockResolvedValue({
74
+ componentsLocation: "app/components/Ui",
75
+ utilsLocation: "app/utils",
76
+ composablesLocation: "app/composables",
77
+ } as any);
78
+ vi.mocked(fetchComponents).mockResolvedValue(mockComponents);
79
+ vi.mocked(fileExists).mockResolvedValue(false);
80
+ vi.mocked(prompts.default).mockResolvedValue({ confirmed: true });
81
+
82
+ await runRemoveCommand(["button"]);
83
+
84
+ expect(vi.mocked(fs.unlink)).not.toHaveBeenCalled();
85
+ expect(vi.mocked(logger.warn)).toHaveBeenCalledWith(expect.stringContaining("Button.vue"));
86
+ });
87
+
88
+ it("should cancel when user declines confirmation", async () => {
89
+ const { fetchComponents } = await import("../../src/utils/fetchComponents");
90
+ const { getUIConfig } = await import("../../src/utils/config");
91
+ const { fileExists } = await import("../../src/utils/fileExists");
92
+ const { logger } = await import("../../src/utils/logger");
93
+ const prompts = await import("prompts");
94
+
95
+ vi.mocked(getUIConfig).mockResolvedValue({
96
+ componentsLocation: "app/components/Ui",
97
+ utilsLocation: "app/utils",
98
+ composablesLocation: "app/composables",
99
+ } as any);
100
+ vi.mocked(fetchComponents).mockResolvedValue(mockComponents);
101
+ vi.mocked(fileExists).mockResolvedValue(true);
102
+ vi.mocked(prompts.default).mockResolvedValue({ confirmed: false });
103
+
104
+ await runRemoveCommand(["button"]);
105
+
106
+ expect(vi.mocked(fs.unlink)).not.toHaveBeenCalled();
107
+ expect(vi.mocked(logger.info)).toHaveBeenCalledWith("Cancelled.");
108
+ });
109
+
110
+ it("should exit with error when no specified names are found in registry", async () => {
111
+ const { fetchComponents } = await import("../../src/utils/fetchComponents");
112
+ const { getUIConfig } = await import("../../src/utils/config");
113
+ const { logger } = await import("../../src/utils/logger");
114
+
115
+ const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);
116
+
117
+ vi.mocked(getUIConfig).mockResolvedValue({
118
+ componentsLocation: "app/components/Ui",
119
+ utilsLocation: "app/utils",
120
+ composablesLocation: "app/composables",
121
+ } as any);
122
+ vi.mocked(fetchComponents).mockResolvedValue(mockComponents);
123
+
124
+ await runRemoveCommand(["nonexistent"]);
125
+
126
+ expect(vi.mocked(logger.error)).toHaveBeenCalledWith(
127
+ "None of the specified components were found in the registry."
128
+ );
129
+ expect(exitSpy).toHaveBeenCalledWith(1);
130
+ });
131
+
132
+ it("should show info and return when no components are installed", async () => {
133
+ const { fetchComponents } = await import("../../src/utils/fetchComponents");
134
+ const { getUIConfig } = await import("../../src/utils/config");
135
+ const { fileExists } = await import("../../src/utils/fileExists");
136
+ const { logger } = await import("../../src/utils/logger");
137
+
138
+ vi.mocked(getUIConfig).mockResolvedValue({
139
+ componentsLocation: "app/components/Ui",
140
+ utilsLocation: "app/utils",
141
+ composablesLocation: "app/composables",
142
+ } as any);
143
+ vi.mocked(fetchComponents).mockResolvedValue(mockComponents);
144
+ vi.mocked(fileExists).mockResolvedValue(false);
145
+
146
+ await runRemoveCommand([]);
147
+
148
+ expect(vi.mocked(logger.info)).toHaveBeenCalledWith("No installed components found.");
149
+ expect(vi.mocked(fs.unlink)).not.toHaveBeenCalled();
150
+ });
151
+ });