pm-auto 1.0.3 → 1.0.5

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 (43) hide show
  1. package/README.md +91 -36
  2. package/dist/build_command.js +1 -1
  3. package/dist/build_command.js.map +1 -1
  4. package/dist/config_path.d.ts +0 -1
  5. package/dist/config_path.d.ts.map +1 -1
  6. package/dist/config_path.js +0 -5
  7. package/dist/config_path.js.map +1 -1
  8. package/dist/config_reader.d.ts +3 -1
  9. package/dist/config_reader.d.ts.map +1 -1
  10. package/dist/config_reader.js +54 -6
  11. package/dist/config_reader.js.map +1 -1
  12. package/dist/display.d.ts +5 -2
  13. package/dist/display.d.ts.map +1 -1
  14. package/dist/display.js +5 -21
  15. package/dist/display.js.map +1 -1
  16. package/dist/index.js +30 -2
  17. package/dist/index.js.map +1 -1
  18. package/dist/install.d.ts +1 -1
  19. package/dist/install.d.ts.map +1 -1
  20. package/dist/install.js +3 -8
  21. package/dist/install.js.map +1 -1
  22. package/dist/orchestrator.d.ts.map +1 -1
  23. package/dist/orchestrator.js +4 -5
  24. package/dist/orchestrator.js.map +1 -1
  25. package/dist/types/index.d.ts +1 -0
  26. package/dist/types/index.d.ts.map +1 -1
  27. package/package.json +5 -3
  28. package/src/build_command.ts +119 -119
  29. package/src/config_path.ts +48 -54
  30. package/src/config_reader.ts +68 -10
  31. package/src/display.ts +1 -7
  32. package/src/index.ts +33 -2
  33. package/src/install.ts +3 -5
  34. package/src/orchestrator.ts +5 -6
  35. package/src/types/index.ts +17 -16
  36. package/test.json +87 -94
  37. package/tests/build_command.test.ts +37 -0
  38. package/tests/config_path.test.ts +120 -0
  39. package/tests/config_reader.test.ts +179 -0
  40. package/tests/display.test.ts +83 -0
  41. package/tests/install.test.ts +200 -0
  42. package/tsconfig.json +3 -2
  43. package/vitest.config.js +8 -0
package/test.json CHANGED
@@ -1,94 +1,87 @@
1
- {
2
- "sample": {
3
- "name": "sample",
4
- "packageManager": "npm",
5
- "packages": [
6
- {
7
- "command": "gsap",
8
- "interactive": false
9
- }
10
- ]
11
- },
12
-
13
- "vite": {
14
- "name": "vite",
15
- "packageManager": "npm",
16
- "packages": [
17
- {
18
- "command": "@types/three --save-dev",
19
- "interactive": false
20
- },
21
- {
22
- "command": "@react-three/fiber",
23
- "interactive": false
24
- },
25
- {
26
- "command": "gsap",
27
- "interactive": false
28
- },
29
- {
30
- "command": "create-vite@latest my-app",
31
- "interactive": true
32
- }
33
- ]
34
- },
35
-
36
- "next": {
37
- "name": "next",
38
- "packageManager": "pnpm",
39
- "packages": [
40
- {
41
- "command": "three --save-dev",
42
- "interactive": false
43
- },
44
- {
45
- "command": "@react-three/drei",
46
- "interactive": false
47
- },
48
- {
49
- "command": "framer-motion",
50
- "interactive": false
51
- },
52
- {
53
- "command": "create-next-app@latest my-app",
54
- "interactive": true
55
- }
56
- ]
57
- },
58
- "test": {
59
- "name": "test",
60
- "packageManager": "npm",
61
- "packages": [
62
- {
63
- "command": "three",
64
- "interactive": false
65
- },
66
- {
67
- "command": "gsap",
68
- "interactive": false
69
- },
70
- {
71
- "command": "@react-three/fiber",
72
- "interactive": false
73
- }
74
- ]
75
- },
76
- "test2": {
77
- "name": "test2",
78
- "packageManager": "npm",
79
- "packages": [
80
- {
81
- "command": "@react-three/drei",
82
- "interactive": false
83
- },
84
- {
85
- "command": "framer-motion",
86
- "interactive": false
87
- },
88
- {
89
- "command": "@react-three/postprocessing",
90
- "interactive": false
91
- }
92
- ]
93
- }
94
- }
1
+ {
2
+ "sample": {
3
+ "name": "sample",
4
+ "description": "",
5
+ "packageManager": "npm",
6
+ "packages": [
7
+ {
8
+ "command": "gsap",
9
+ "interactive": false
10
+ }
11
+ ]
12
+ },
13
+
14
+ "vite": {
15
+ "name": "vite",
16
+ "description": "Vite configuration",
17
+ "packageManager": "npm",
18
+ "packages": [
19
+ {
20
+ "command": "create-vite@latest my-app",
21
+ "interactive": true
22
+ }
23
+ ]
24
+ },
25
+
26
+ "next": {
27
+ "name": "next",
28
+ "description": "Next.js configuration",
29
+ "packageManager": "pnpm",
30
+ "packages": [
31
+ {
32
+ "command": "three --save-dev",
33
+ "interactive": false
34
+ },
35
+ {
36
+ "command": "@react-three/drei",
37
+ "interactive": false
38
+ },
39
+ {
40
+ "command": "framer-motion",
41
+ "interactive": false
42
+ },
43
+ {
44
+ "command": "create-next-app@latest my-app",
45
+ "interactive": true
46
+ }
47
+ ]
48
+ },
49
+ "test": {
50
+ "name": "test",
51
+ "description": "Test configuration",
52
+ "packageManager": "npm",
53
+ "packages": [
54
+ {
55
+ "command": "three",
56
+ "interactive": false
57
+ },
58
+ {
59
+ "command": "gsap",
60
+ "interactive": false
61
+ },
62
+ {
63
+ "command": "@react-three/fiber",
64
+ "interactive": false
65
+ }
66
+ ]
67
+ },
68
+ "test2": {
69
+ "name": "test2",
70
+ "description": "Test2 configuration",
71
+ "packageManager": "npm",
72
+ "packages": [
73
+ {
74
+ "command": "@react-three/drei",
75
+ "interactive": false
76
+ },
77
+ {
78
+ "command": "framer-motion",
79
+ "interactive": false
80
+ },
81
+ {
82
+ "command": "@react-three/postprocessing",
83
+ "interactive": false
84
+ }
85
+ ]
86
+ }
87
+ }
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { buildCommands, buildUninstallCommands } from "../src/build_command.js";
3
+ import type { ConfigType } from "../src/types/index.js";
4
+
5
+ describe("buildCommand", () => {
6
+ const mockConfig: ConfigType[] = [
7
+ {
8
+ name: "vite",
9
+ packageManager: "npm",
10
+ packages: [
11
+ { command: "vite@latest", interactive: true },
12
+ { command: "gsap@latest", interactive: false },
13
+ ],
14
+ },
15
+ ];
16
+ it("builds an installation command", () => {
17
+ const command = buildCommands(mockConfig);
18
+ expect(command).toEqual([
19
+ {
20
+ name: "vite",
21
+ interactive: ["npx vite@latest"],
22
+ nonInteractive: ["npm install gsap@latest"],
23
+ },
24
+ ]);
25
+ });
26
+
27
+ it("builds an uninstallation command", () => {
28
+ const command = buildUninstallCommands(mockConfig);
29
+ expect(command).toEqual([
30
+ {
31
+ name: "vite",
32
+ interactive: [],
33
+ nonInteractive: ["npm uninstall gsap@latest"],
34
+ },
35
+ ]);
36
+ });
37
+ });
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import * as fs from "fs";
3
+ import * as os from "os";
4
+ import { saveConfigPath, getConfigPath } from "../src/config_path.js";
5
+ import * as display from "../src/display.js";
6
+
7
+ const mocks = vi.hoisted(() => {
8
+ return {
9
+ homedir: vi.fn(() => "/mock/home"),
10
+ };
11
+ });
12
+
13
+ // Mock modules FIRST
14
+ vi.mock("fs");
15
+ vi.mock("os", () => ({
16
+ homedir: mocks.homedir,
17
+ }));
18
+ vi.mock("../src/display.js");
19
+
20
+ // NOW import the module under test AFTER mocks are configured
21
+
22
+ describe("Config Path Management", () => {
23
+ const mockHomeDir = "/mock/home";
24
+ const mockSettingsDir = "\\mock\\home\\.pm-auto";
25
+ const mockSettingsFile = "\\mock\\home\\.pm-auto\\settings.json";
26
+
27
+ beforeEach(() => {
28
+ vi.resetAllMocks();
29
+ vi.mocked(os.homedir).mockReturnValue(mockHomeDir);
30
+ });
31
+
32
+ describe("saveConfigPath", () => {
33
+ it("creates directory and saves config path", () => {
34
+ vi.mocked(fs.existsSync).mockReturnValue(false);
35
+ vi.mocked(fs.realpathSync).mockReturnValue("\\real\\path\\config.json");
36
+
37
+ saveConfigPath("./config.json");
38
+
39
+ expect(fs.mkdirSync).toHaveBeenCalledWith(mockSettingsDir, {
40
+ recursive: true,
41
+ });
42
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
43
+ mockSettingsFile,
44
+ expect.stringContaining('"configPath"'),
45
+ );
46
+ expect(display.display).toHaveBeenCalledWith(
47
+ expect.any(String),
48
+ "success",
49
+ );
50
+ });
51
+
52
+ it("skips directory creation if exists", () => {
53
+ vi.mocked(fs.existsSync).mockReturnValue(true);
54
+ vi.mocked(fs.realpathSync).mockReturnValue("/real/path");
55
+
56
+ saveConfigPath("/path");
57
+
58
+ expect(fs.mkdirSync).not.toHaveBeenCalled();
59
+ });
60
+
61
+ it("handles errors gracefully", () => {
62
+ vi.mocked(fs.realpathSync).mockImplementation(() => {
63
+ throw new Error("File not found");
64
+ });
65
+
66
+ saveConfigPath("/invalid/path");
67
+
68
+ expect(display.display).toHaveBeenCalledWith(
69
+ expect.stringContaining("File not found"),
70
+ "error",
71
+ );
72
+ });
73
+ });
74
+
75
+ describe("getConfigPath", () => {
76
+ it("returns config path when file exists", () => {
77
+ vi.mocked(fs.existsSync).mockReturnValue(true);
78
+ vi.mocked(fs.readFileSync).mockReturnValue(
79
+ JSON.stringify({ configPath: "/saved/path" }),
80
+ );
81
+
82
+ const result = getConfigPath();
83
+
84
+ expect(result).toBe("/saved/path");
85
+ });
86
+
87
+ it("shows error when settings file does not exist", () => {
88
+ vi.mocked(fs.existsSync).mockReturnValue(false);
89
+
90
+ const result = getConfigPath();
91
+
92
+ expect(display.display).toHaveBeenCalledWith(
93
+ expect.stringContaining("Config file path not set"),
94
+ "error",
95
+ );
96
+ expect(result).toBeUndefined();
97
+ });
98
+
99
+ it("handles JSON parse errors", () => {
100
+ vi.mocked(fs.existsSync).mockReturnValue(true);
101
+ vi.mocked(fs.readFileSync).mockReturnValue("invalid json");
102
+
103
+ getConfigPath();
104
+
105
+ expect(display.display).toHaveBeenCalledWith(
106
+ expect.stringContaining("Error reading"),
107
+ "error",
108
+ );
109
+ });
110
+
111
+ it("returns empty string if configPath is missing", () => {
112
+ vi.mocked(fs.existsSync).mockReturnValue(true);
113
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({}));
114
+
115
+ const result = getConfigPath();
116
+
117
+ expect(result).toBe("");
118
+ });
119
+ });
120
+ });
@@ -0,0 +1,179 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import fs from "fs/promises";
3
+ import * as fsd from "fs";
4
+ import * as path from "path";
5
+ import { display } from "../src/display.js";
6
+ import { detectPackageManager, getConfigObject } from "../src/config_reader.js";
7
+ import { getConfigPath } from "../src/config_path.js";
8
+ import type { CommandResult, ConfigType } from "../src/types/index.js";
9
+ import * as clack from "@clack/prompts";
10
+
11
+ vi.mock("fs/promises");
12
+ vi.mock("fs");
13
+ vi.mock("../src/config_path.js");
14
+ vi.mock("../src/display.js");
15
+ vi.mock("@clack/prompts");
16
+ vi.mock("../src/config_path.js");
17
+
18
+ describe("detect package manager", () => {
19
+ beforeEach(() => {
20
+ vi.resetAllMocks();
21
+ });
22
+ it("should detects pnpm from lock file", () => {
23
+ vi.mocked(fsd.existsSync).mockImplementation((filePath) =>
24
+ filePath.toString().includes("pnpm-lock.yaml"),
25
+ );
26
+ const result = detectPackageManager("/test/path");
27
+
28
+ expect(result).toBe("pnpm");
29
+ });
30
+ it("should detects yarn from lock file", () => {
31
+ vi.mocked(fsd.existsSync).mockImplementation((filePath) =>
32
+ filePath.toString().includes("yarn.lock"),
33
+ );
34
+ const result = detectPackageManager("/test/path");
35
+
36
+ expect(result).toBe("yarn");
37
+ });
38
+ it("should detects npm from lock file", () => {
39
+ vi.mocked(fsd.existsSync).mockImplementation((filePath) =>
40
+ filePath.toString().includes("package-lock.json"),
41
+ );
42
+ const result = detectPackageManager("/test/path");
43
+
44
+ expect(result).toBe("npm");
45
+ });
46
+ it("should display error message when no lock file found", () => {
47
+ vi.mocked(fsd.existsSync).mockImplementation(() => false);
48
+ const result = detectPackageManager("/test/path");
49
+
50
+ expect(display).toHaveBeenCalledWith(
51
+ expect.stringContaining("No Lock File Found"),
52
+ "error",
53
+ );
54
+ expect(result).toBeUndefined();
55
+ });
56
+ });
57
+
58
+ describe("get Config Object", () => {
59
+ const mockConfig = {
60
+ react: {
61
+ name: "react",
62
+ packages: [{ command: "npm install react", interactive: false }],
63
+ },
64
+ vue: {
65
+ name: "vue",
66
+ packages: [{ command: "npm install vue", interactive: false }],
67
+ },
68
+ };
69
+
70
+ beforeEach(() => {
71
+ vi.resetAllMocks();
72
+ });
73
+
74
+ it("should get config object the user wants and display warning when package does not exist", async () => {
75
+ vi.mocked(getConfigPath).mockResolvedValue("/path/to/test");
76
+ vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockConfig));
77
+
78
+ const config = await getConfigObject(["next", "react", "vue"], {});
79
+
80
+ expect(config).toEqual([
81
+ {
82
+ name: "react",
83
+ packages: [{ command: "npm install react", interactive: false }],
84
+ },
85
+ {
86
+ name: "vue",
87
+ packages: [{ command: "npm install vue", interactive: false }],
88
+ },
89
+ ]);
90
+ expect(display).toHaveBeenCalledWith(
91
+ expect.stringContaining("Package 'next' not found in config"),
92
+ "warning",
93
+ );
94
+ });
95
+
96
+ // it("should display an error on readFile error", async () => {
97
+ // vi.mocked(getConfigPath).mockReturnValue("/path/to/test");
98
+ // vi.mocked(fs.readFile).mockRejectedValue(new Error("File not found"));
99
+ // await getConfigObject([], {});
100
+ // expect(display).toHaveBeenCalledWith(
101
+ // expect.stringContaining("Try updating the config file"),
102
+ // "error",
103
+ // );
104
+ // });
105
+ //
106
+
107
+ it("adds command with addCommand option", async () => {
108
+ vi.mocked(getConfigPath).mockReturnValue("/mock/config.json");
109
+ vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockConfig));
110
+
111
+ const result = (await getConfigObject(["react"], {
112
+ addCommand: "--save-dev",
113
+ })) as ConfigType[];
114
+
115
+ if (result[0]?.packages[0]?.command) {
116
+ expect(result[0].packages[0].command).toContain("--save-dev");
117
+ }
118
+ });
119
+
120
+ it("shows dry run preview and continues on confirm", async () => {
121
+ vi.mocked(getConfigPath).mockReturnValue("/mock/config.json");
122
+ vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockConfig));
123
+ vi.mocked(clack.confirm).mockResolvedValue(true);
124
+ vi.mocked(clack.isCancel).mockReturnValue(false);
125
+
126
+ const result = await getConfigObject(["react"], { dryRun: true });
127
+
128
+ expect(display).toHaveBeenCalledWith("Dry Run:", "info");
129
+ expect(clack.confirm).toHaveBeenCalled();
130
+ expect(result).toHaveLength(1);
131
+ });
132
+
133
+ it("cancels on dry run cancel", async () => {
134
+ vi.mocked(getConfigPath).mockReturnValue("/mock/config.json");
135
+ vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockConfig));
136
+ vi.mocked(clack.confirm).mockResolvedValue(false);
137
+ vi.mocked(clack.isCancel).mockReturnValue(true);
138
+
139
+ const mockExit = vi
140
+ .spyOn(process, "exit")
141
+ .mockImplementation(() => undefined as never);
142
+
143
+ await getConfigObject(["react"], { dryRun: true });
144
+
145
+ expect(clack.cancel).toHaveBeenCalledWith("Operation cancelled.");
146
+ expect(mockExit).toHaveBeenCalledWith(0);
147
+
148
+ mockExit.mockRestore();
149
+ });
150
+
151
+ it("generates command for package.json install", async () => {
152
+ vi.mocked(fsd.existsSync).mockImplementation((filePath) =>
153
+ filePath.toString().includes("package-lock.json"),
154
+ );
155
+
156
+ const result = await getConfigObject([], { pkgJson: true });
157
+
158
+ expect(result).toEqual([
159
+ {
160
+ name: "package.json",
161
+ interactive: [],
162
+ nonInteractive: ["npm install"],
163
+ },
164
+ ]);
165
+ });
166
+
167
+ it("uses correct package manager for package.json", async () => {
168
+ vi.mocked(fsd.existsSync).mockImplementation((filePath) =>
169
+ filePath.toString().includes("pnpm-lock.yaml"),
170
+ );
171
+
172
+ const result = (await getConfigObject([], {
173
+ pkgJson: true,
174
+ })) as CommandResult[];
175
+ if (result[0]) {
176
+ expect(result[0].nonInteractive[0]).toBe("pnpm install");
177
+ }
178
+ });
179
+ });
@@ -0,0 +1,83 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { display } from "../src/display.js";
3
+ import { log, spinner } from "@clack/prompts";
4
+
5
+ // Mock the dependencies
6
+ vi.mock("@clack/prompts", () => ({
7
+ log: {
8
+ error: vi.fn(),
9
+ success: vi.fn(),
10
+ warn: vi.fn(),
11
+ info: vi.fn(),
12
+ message: vi.fn(),
13
+ },
14
+ spinner: vi.fn(),
15
+ }));
16
+
17
+ vi.mock("chalk", () => ({
18
+ default: {
19
+ red: vi.fn((text) => `red:${text}`),
20
+ green: vi.fn((text) => `green:${text}`),
21
+ yellow: vi.fn((text) => `yellow:${text}`),
22
+ blue: vi.fn((text) => `blue:${text}`),
23
+ },
24
+ }));
25
+
26
+ describe("display", () => {
27
+ let mockSpinner: any;
28
+ let exitSpy: any;
29
+
30
+ beforeEach(() => {
31
+ mockSpinner = {
32
+ stop: vi.fn(),
33
+ start: vi.fn(),
34
+ };
35
+ vi.mocked(spinner).mockReturnValue(mockSpinner);
36
+ exitSpy = vi
37
+ .spyOn(process, "exit")
38
+ .mockImplementation(() => undefined as never);
39
+ });
40
+
41
+ afterEach(() => {
42
+ vi.clearAllMocks();
43
+ exitSpy.mockRestore();
44
+ });
45
+
46
+ it("should display error message and exit process", () => {
47
+ display("Error text", "error");
48
+ expect(log.error).toHaveBeenCalledWith("red:Error text");
49
+ expect(exitSpy).toHaveBeenCalledWith(0);
50
+ });
51
+
52
+ it("should display success message", () => {
53
+ display("Success text", "success");
54
+ expect(log.success).toHaveBeenCalledWith("green:Success text");
55
+ expect(exitSpy).not.toHaveBeenCalled();
56
+ });
57
+
58
+ it("should display warning message", () => {
59
+ display("Warning text", "warning");
60
+ expect(log.warn).toHaveBeenCalledWith("yellow:Warning text");
61
+ });
62
+
63
+ it("should display info message", () => {
64
+ display("Info text", "info");
65
+ expect(log.info).toHaveBeenCalledWith("blue:Info text");
66
+ });
67
+
68
+ it("should start spinner for loading type", () => {
69
+ const result = display("Loading text", "loading");
70
+ expect(mockSpinner.start).toHaveBeenCalledWith("Loading text");
71
+ expect(result).toBe(mockSpinner);
72
+ });
73
+
74
+ it("should display plain message for default/empty type", () => {
75
+ display("Plain text", "");
76
+ expect(log.message).toHaveBeenCalledWith("Plain text");
77
+ });
78
+
79
+ it("should display plain message when type is not recognized", () => {
80
+ display("Unknown text", "unknown" as any);
81
+ expect(log.message).toHaveBeenCalledWith("Unknown text");
82
+ });
83
+ });