evophp 1.3.1 → 1.3.3

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 CHANGED
@@ -16,6 +16,7 @@ EvoPHP CLI is a command-line interface designed to simplify the development and
16
16
  * [Plugin/Theme Uninstallation](#plugintheme-uninstallation)
17
17
  * [Dependency Management](#dependency-management)
18
18
  * [Entry Management](#entry-management)
19
+ * [Plugin & Theme Dependencies](#plugin--theme-dependencies)
19
20
  * [Authentication & PAT Storage](#authentication--pat-storage)
20
21
  * [Further Documentation](#further-documentation)
21
22
 
@@ -104,6 +105,8 @@ evophp i theme <name>
104
105
  evophp i theme johndoe/EvoTheme
105
106
  ```
106
107
 
108
+ * **Automatic dependency installation**: If the plugin or theme includes a `dependencies.json` file, EvoPHP will automatically install all listed JS/PHP packages and any dependent EvoPHP plugins or themes. See [Plugin & Theme Dependencies](#plugin--theme-dependencies).
109
+
107
110
  ---
108
111
 
109
112
  ### Plugin/Theme Uninstallation
@@ -116,6 +119,7 @@ evophp u theme <name>
116
119
  ```
117
120
 
118
121
  * Removes the installed plugin or theme from the project.
122
+ * Also removes any EvoPHP plugin/theme dependencies that were installed alongside it — **unless** they are still required by another installed plugin or theme, in which case they are retained.
119
123
 
120
124
  * Examples:
121
125
 
@@ -133,7 +137,7 @@ evophp install-dependencies <plugin-name>
133
137
  evophp d <plugin-name>
134
138
  ```
135
139
 
136
- * Adds or installs plugin dependencies.
140
+ * Adds or installs plugin dependencies — npm packages, Composer packages, or other EvoPHP plugins/themes.
137
141
 
138
142
  #### Production (npm):
139
143
 
@@ -153,6 +157,25 @@ evophp d MyPlugin --dev
153
157
  evophp d MyPlugin --composer
154
158
  ```
155
159
 
160
+ #### EvoPHP Plugin dependency:
161
+
162
+ ```bash
163
+ evophp d MyPlugin --evoplugin
164
+ ```
165
+
166
+ * Prompts for one or more `author/name` (or plain `name`) EvoPHP plugins to add as dependencies.
167
+ * Clones and installs each listed plugin into the project.
168
+ * Records them under the `"plugin"` key in `MyPlugin`'s `dependencies.json`.
169
+ * A GitHub token is only required when a dependency is provided without an `author/` prefix.
170
+
171
+ #### EvoPHP Theme dependency:
172
+
173
+ ```bash
174
+ evophp d MyPlugin --evotheme
175
+ ```
176
+
177
+ * Same as `--evoplugin` but for themes — clones, installs, and records under the `"theme"` key in `dependencies.json`.
178
+
156
179
  ---
157
180
 
158
181
  ### Entry Management
@@ -190,6 +213,59 @@ This is used to update core libraries of EvoPhp.
190
213
 
191
214
  ---
192
215
 
216
+ ## Plugin & Theme Dependencies
217
+
218
+ EvoPHP supports a `dependencies.json` file at the root of any plugin or theme. This file declares all dependencies that should be automatically resolved during installation.
219
+
220
+ ### Structure
221
+
222
+ ```json
223
+ {
224
+ "prod": "qrcode.vue vue3-qrcode-reader jspdf @fullcalendar/core @fullcalendar/vue3",
225
+ "dev": "",
226
+ "plugin": "kinsokafor/Invoice kinsokafor/GooglePack",
227
+ "theme": "kinsokafor/BSTTheme"
228
+ }
229
+ ```
230
+
231
+ | Key | Type | Description |
232
+ |---|---|---|
233
+ | `prod` | string | Space-separated npm packages to install as production dependencies |
234
+ | `dev` | string | Space-separated npm packages to install as dev dependencies |
235
+ | `plugin` | string | Space-separated `author/name` EvoPHP plugins this plugin/theme depends on |
236
+ | `theme` | string | Space-separated `author/name` EvoPHP themes this plugin/theme depends on |
237
+
238
+ ### Automatic Installation
239
+
240
+ When you run `evophp install`, EvoPHP will:
241
+
242
+ 1. Clone the plugin or theme from GitHub.
243
+ 2. Read its `dependencies.json` (if present).
244
+ 3. Install all listed npm (`prod`/`dev`) and Composer (`prod`) packages.
245
+ 4. Recursively install any EvoPHP plugins or themes listed under `"plugin"` and `"theme"` — each of which may declare their own `dependencies.json`, resolved the same way.
246
+
247
+ ### Automatic Uninstallation
248
+
249
+ When you run `evophp uninstall`, EvoPHP will:
250
+
251
+ 1. Remove the target plugin or theme.
252
+ 2. Check each of its EvoPHP plugin/theme dependencies.
253
+ 3. Remove a dependency only if **no other installed plugin or theme** still requires it. If another plugin depends on it, it is retained and a notice is shown.
254
+
255
+ Example:
256
+
257
+ ```
258
+ PluginA depends on → Invoice, GooglePack
259
+ PluginB depends on → Invoice
260
+
261
+ evophp uninstall plugin PluginA
262
+ ✔ PluginA removed
263
+ ⚠ Invoice retained (still required by PluginB)
264
+ ✔ GooglePack removed
265
+ ```
266
+
267
+ ---
268
+
193
269
  ## Authentication & PAT Storage
194
270
 
195
271
  When using a command with just `<name>` (e.g. `evophp new plugin MyPlugin`), EvoPHP CLI will:
@@ -207,4 +283,4 @@ When using a command with just `<name>` (e.g. `evophp new plugin MyPlugin`), Evo
207
283
  ## Further Documentation
208
284
 
209
285
  More advanced use cases, templates, and configuration details coming soon at:
210
- 📘 [Official EvoPHP Documentation](#)
286
+ 📘 [Official EvoPHP Documentation](#)
package/bin/evophp.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  const yargs = require("yargs");
4
4
  const { hideBin } = require("yargs/helpers");
@@ -2,38 +2,217 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { execSync } = require("child_process");
4
4
  const { readLineAsync } = require("../utils/helpers");
5
+ const { getUserData } = require("../utils/github");
6
+ const { cloneRepo } = require("../utils/git");
7
+ const { installDependencies, extractDependencies } = require("../utils/dependencies");
5
8
  const log = require("../utils/logger");
9
+ require("dotenv").config({ path: "./.env.development" });
10
+
11
+ /**
12
+ * Installs a single evophp plugin or theme and records it in the
13
+ * parent plugin's dependencies.json under the appropriate key.
14
+ *
15
+ * @param {'plugin'|'theme'} depType
16
+ * @param {string} dep - "author/name" or plain "name"
17
+ * @param {string} gitHubToken
18
+ * @param {object} dependencies - the in-memory dependencies object to mutate
19
+ */
20
+ async function installEvoDep(depType, dep, gitHubToken, dependencies) {
21
+ let author, depName;
22
+
23
+ if (dep.includes("/")) {
24
+ [author, depName] = dep.split("/");
25
+ } else {
26
+ if (!gitHubToken) {
27
+ log.error(`A GitHub token is required to install "${dep}" without an author prefix.`);
28
+ process.exit(1);
29
+ }
30
+ const { getUserData } = require("../utils/github");
31
+ const userData = await getUserData(gitHubToken);
32
+ author = userData.login;
33
+ depName = dep;
34
+ }
35
+
36
+ const paths = depType === "plugin"
37
+ ? [path.join("Public", "Modules", depName), path.join("EvoJsDev", "Modules", depName)]
38
+ : [path.join("Public", "Themes", depName)];
39
+
40
+ if (paths.every(p => fs.existsSync(p))) {
41
+ log.warn(`${depType} "${depName}" is already installed, skipping clone.`);
42
+ } else {
43
+ log.info(`Installing ${depType} "${depName}" from ${author}...`);
44
+
45
+ if (depType === "plugin") {
46
+ await cloneRepo(`${author}/${depName}`, null, paths[0]);
47
+ await cloneRepo(`${author}/${depName}.js`, null, paths[1]);
48
+ await installDependencies(extractDependencies(paths[1], "js"), "js");
49
+ await installDependencies(extractDependencies(paths[0], "php"), "php");
50
+ await execSync(`evophp d ${author}/${depName} --sync`, { stdio: "inherit" });
51
+ } else {
52
+ await cloneRepo(`${author}/${depName}`, null, paths[0]);
53
+ }
54
+
55
+ log.success(`${depType} "${depName}" installed.`);
56
+ }
57
+
58
+ // Record as "author/name" in dependencies.json, deduplicated
59
+ const key = depType; // "plugin" or "theme"
60
+ const existing = (dependencies[key] || "")
61
+ .split(" ")
62
+ .map(d => d.trim())
63
+ .filter(Boolean);
64
+
65
+ const fullRef = `${author}/${depName}`;
66
+ if (!existing.some(d => d === fullRef || d.split("/").pop() === depName)) {
67
+ existing.push(fullRef);
68
+ }
69
+ dependencies[key] = existing.join(" ");
70
+ }
6
71
 
7
72
  module.exports = {
8
- command: "install-dependencies <plugin> [--dev|--composer]",
73
+ command: "install-dependencies <plugin> [--dev|--composer|--evoplugin|--evotheme|--sync]",
9
74
  aliases: ["d"],
10
75
  describe: "Add and install plugin or theme dependencies",
11
76
 
12
77
  builder: yargs =>
13
78
  yargs
14
79
  .positional("plugin", { describe: "Plugin or theme name", type: "string" })
15
- .option("dev", { describe: "Install as dev dependency (JS)", type: "boolean" })
16
- .option("composer", { describe: "Use composer for PHP", type: "boolean" }),
80
+ .option("dev", { describe: "Install as dev dependency (JS)", type: "boolean" })
81
+ .option("composer", { describe: "Use composer for PHP", type: "boolean" })
82
+ .option("evoplugin", { describe: "Install an evophp plugin dependency", type: "boolean" })
83
+ .option("evotheme", { describe: "Install an evophp theme dependency", type: "boolean" })
84
+ .option("sync", { describe: "Install any missing evophp plugins/themes in dependencies.json", type: "boolean" }),
17
85
 
18
- handler: async ({ plugin, dev, composer }) => {
86
+ handler: async ({ plugin, dev, composer, evoplugin, evotheme, sync }) => {
19
87
  try {
88
+ const isEvo = !!(evoplugin || evotheme);
89
+ const isPHP = !!composer;
20
90
  const depType = dev ? "dev" : "prod";
21
91
 
22
- const isPHP = !!composer;
92
+ // ── sync: install any missing evophp plugins/themes from dependencies.json ──
93
+ if (sync) {
94
+ const evoDepsDir = path.join("EvoJsDev", "Modules", plugin);
95
+
96
+ if (!fs.existsSync(evoDepsDir)) {
97
+ log.error(`Plugin not found at ${evoDepsDir}`);
98
+ process.exit(1);
99
+ }
100
+
101
+ const dependenciesFile = path.join(evoDepsDir, "dependencies.json");
102
+ if (!fs.existsSync(dependenciesFile)) {
103
+ log.warn(`No dependencies.json found for "${plugin}".`);
104
+ return;
105
+ }
106
+
107
+ const dependencies = JSON.parse(fs.readFileSync(dependenciesFile, "utf-8"));
108
+
109
+ const pluginDeps = (dependencies.plugin || "").split(" ").map(d => d.trim()).filter(Boolean);
110
+ const themeDeps = (dependencies.theme || "").split(" ").map(d => d.trim()).filter(Boolean);
111
+
112
+ if (pluginDeps.length === 0 && themeDeps.length === 0) {
113
+ log.info("No evophp plugin or theme dependencies listed.");
114
+ return;
115
+ }
116
+
117
+ // Collect only the missing ones
118
+ const missingPlugins = pluginDeps.filter(dep => {
119
+ const depName = dep.includes("/") ? dep.split("/").pop() : dep;
120
+ return !fs.existsSync(path.join("Public", "Modules", depName)) ||
121
+ !fs.existsSync(path.join("EvoJsDev", "Modules", depName));
122
+ });
123
+
124
+ const missingThemes = themeDeps.filter(dep => {
125
+ const depName = dep.includes("/") ? dep.split("/").pop() : dep;
126
+ return !fs.existsSync(path.join("Public", "Themes", depName));
127
+ });
128
+
129
+ if (missingPlugins.length === 0 && missingThemes.length === 0) {
130
+ log.success("All evophp dependencies are already installed.");
131
+ return;
132
+ }
133
+
134
+ log.info(`Missing plugins : ${missingPlugins.join(", ") || "none"}`);
135
+ log.info(`Missing themes : ${missingThemes.join(", ") || "none"}`);
136
+
137
+ // Resolve a token only if needed
138
+ let gitHubToken = process.env.GITHUB_TOKEN;
139
+ const needsToken = [...missingPlugins, ...missingThemes].some(d => !d.includes("/"));
140
+ if (needsToken && !gitHubToken) {
141
+ gitHubToken = await readLineAsync("Enter your GitHub Personal Access Token: ");
142
+ const { saveToEnv } = require("../utils/helpers");
143
+ saveToEnv("GITHUB_TOKEN", gitHubToken);
144
+ log.success("GitHub token saved to .env.development");
145
+ }
146
+
147
+ for (const dep of missingPlugins) {
148
+ await installEvoDep("plugin", dep, gitHubToken, dependencies);
149
+ }
150
+ for (const dep of missingThemes) {
151
+ await installEvoDep("theme", dep, gitHubToken, dependencies);
152
+ }
153
+
154
+ log.success(`Sync complete for "${plugin}".`);
155
+ return;
156
+ }
157
+
158
+ // For JS/PHP deps the source of truth lives in EvoJsDev; for PHP-only in Public
23
159
  const baseDir = isPHP
24
160
  ? path.join("Public", "Modules", plugin)
25
161
  : path.join("EvoJsDev", "Modules", plugin);
26
162
 
27
- if (!fs.existsSync(baseDir)) {
28
- log.error(`Plugin not found at ${baseDir}`);
163
+ // evophp deps are recorded in the JS side of the plugin
164
+ const evoDepsDir = path.join("EvoJsDev", "Modules", plugin);
165
+
166
+ const targetDir = isEvo ? evoDepsDir : baseDir;
167
+
168
+ if (!fs.existsSync(targetDir)) {
169
+ log.error(`Plugin not found at ${targetDir}`);
29
170
  process.exit(1);
30
171
  }
31
172
 
32
- const dependenciesFile = path.join(baseDir, "dependencies.json");
173
+ const dependenciesFile = path.join(targetDir, "dependencies.json");
33
174
  let dependencies = fs.existsSync(dependenciesFile)
34
175
  ? JSON.parse(fs.readFileSync(dependenciesFile, "utf-8"))
35
176
  : {};
36
177
 
178
+ // ── evophp plugin / theme dependencies ────────────────────────────────
179
+ if (isEvo) {
180
+ const evoDepType = evoplugin ? "plugin" : "theme";
181
+
182
+ log.info(`Existing evophp ${evoDepType} dependencies:`);
183
+ log.info(dependencies[evoDepType] || "None");
184
+
185
+ const input = await readLineAsync(
186
+ `Enter evophp ${evoDepType}(s) to add as dependencies (space-separated, author/name or name): `
187
+ );
188
+ const newDeps = input.split(" ").map(d => d.trim()).filter(Boolean);
189
+
190
+ if (newDeps.length === 0) {
191
+ log.warn("No dependencies provided.");
192
+ return;
193
+ }
194
+
195
+ let gitHubToken = process.env.GITHUB_TOKEN;
196
+ // Only fetch a token if any dep is missing the author prefix
197
+ if (newDeps.some(d => !d.includes("/"))) {
198
+ if (!gitHubToken) {
199
+ gitHubToken = await readLineAsync("Enter your GitHub Personal Access Token: ");
200
+ const { saveToEnv } = require("../utils/helpers");
201
+ saveToEnv("GITHUB_TOKEN", gitHubToken);
202
+ log.success("GitHub token saved to .env.development");
203
+ }
204
+ }
205
+
206
+ for (const dep of newDeps) {
207
+ await installEvoDep(evoDepType, dep, gitHubToken, dependencies);
208
+ }
209
+
210
+ fs.writeFileSync(dependenciesFile, JSON.stringify(dependencies, null, 2));
211
+ log.success("Updated dependencies.json with new evophp dependencies.");
212
+ return;
213
+ }
214
+
215
+ // ── JS / PHP (npm / composer) dependencies ────────────────────────────
37
216
  log.info(`Existing ${isPHP ? "PHP" : "JS"} dependencies:`);
38
217
  log.info("Production:", dependencies.prod || "None");
39
218
  if (!isPHP) log.info("Development:", dependencies.dev || "None");
@@ -64,7 +243,6 @@ module.exports = {
64
243
  fs.writeFileSync(dependenciesFile, JSON.stringify(dependencies, null, 2));
65
244
  log.success("Updated dependencies.json");
66
245
 
67
- // Install
68
246
  const installCmd = isPHP
69
247
  ? `composer require ${newDeps.join(" ")}`
70
248
  : `npm install ${newDeps.join(" ")} ${dev ? "--save-dev" : "--save"}`;
@@ -77,4 +255,4 @@ module.exports = {
77
255
  process.exit(1);
78
256
  }
79
257
  }
80
- };
258
+ };
package/commands/init.js CHANGED
@@ -3,6 +3,7 @@ const path = require("path");
3
3
  const { execSync } = require("child_process");
4
4
  const { readLineAsync } = require("../utils/helpers");
5
5
  const log = require("../utils/logger");
6
+ // const { editFile } = require("../utils/edit");
6
7
 
7
8
  module.exports = {
8
9
  command: "init [name]",
@@ -4,10 +4,102 @@ const { execSync } = require('child_process');
4
4
  const { readLineAsync, mergeConfig, saveToEnv } = require('../utils/helpers');
5
5
  const { getUserData } = require('../utils/github');
6
6
  const { cloneRepo } = require('../utils/git');
7
- const { installDependencies } = require('../utils/dependencies');
7
+ const { installDependencies, extractDependencies, mergeDependencies } = require('../utils/dependencies');
8
8
  const log = require('../utils/logger');
9
9
  require("dotenv").config({ path: "./.env.development" });
10
10
 
11
+ let jsDep = {};
12
+ let phpDep = {};
13
+ let configFiles = [];
14
+ /**
15
+ * Core install logic for a single plugin or theme.
16
+ * Extracted so it can be called recursively for evophp dependencies.
17
+ *
18
+ * @param {'plugin'|'theme'} type
19
+ * @param {string} author
20
+ * @param {string} pluginName
21
+ * @param {string} gitHubToken
22
+ * @param {Set<string>} installed - tracks already-installed entries to avoid infinite loops
23
+ */
24
+ async function installOne(type, author, pluginName, gitHubToken, installed = new Set()) {
25
+ const key = `${type}:${author}/${pluginName}`;
26
+ if (installed.has(key)) {
27
+ log.info(`Skipping already-installed ${type} "${author}/${pluginName}"`);
28
+ return;
29
+ }
30
+ installed.add(key);
31
+
32
+ const paths = type === 'plugin'
33
+ ? [path.join('Public', 'Modules', pluginName), path.join('EvoJsDev', 'Modules', pluginName)]
34
+ : [path.join('Public', 'Themes', pluginName)];
35
+
36
+ // Skip clone if already present on disk
37
+ if (paths.every(p => fs.existsSync(p))) {
38
+ log.warn(`${type} "${pluginName}" already exists on disk, skipping clone.`);
39
+ } else {
40
+ log.info(`Installing ${type} "${pluginName}" from ${author}...`);
41
+
42
+ if (type === 'plugin') {
43
+ await cloneRepo(`${author}/${pluginName}`, null, paths[0]);
44
+ await cloneRepo(`${author}/${pluginName}.js`, null, paths[1]);
45
+
46
+ const configFile = path.resolve(`./EvoJsDev/Modules/${pluginName}/config.json`);
47
+ if (fs.existsSync(configFile)) {
48
+ const pluginConfig = require(configFile);
49
+ mergeConfig(pluginConfig, "./config.json", false);
50
+ }
51
+
52
+ let newJsDep = await extractDependencies(paths[1], 'js');
53
+ jsDep = await mergeDependencies(newJsDep, jsDep);
54
+
55
+ let newPhpDep = await extractDependencies(paths[0], 'php');
56
+ phpDep = await mergeDependencies(newPhpDep, phpDep);
57
+
58
+ } else if (type === 'theme') {
59
+ await cloneRepo(`${author}/${pluginName}`, null, paths[0]);
60
+ }
61
+ }
62
+
63
+ // Read dependencies.json to find dependent evophp plugins/themes
64
+ const depFile = type === 'plugin'
65
+ ? path.join('EvoJsDev', 'Modules', pluginName, 'dependencies.json')
66
+ : path.join('Public', 'Themes', pluginName, 'dependencies.json');
67
+
68
+ if (!fs.existsSync(depFile)) return;
69
+
70
+ let deps;
71
+ try {
72
+ deps = JSON.parse(fs.readFileSync(depFile, 'utf-8'));
73
+ } catch (err) {
74
+ log.warn(`Could not parse dependencies.json for ${type} "${pluginName}": ${err.message}`);
75
+ return;
76
+ }
77
+
78
+ // Recursively install dependent evophp plugins
79
+ const pluginDeps = (deps.plugin || '').split(' ').map(d => d.trim()).filter(Boolean);
80
+ for (const dep of pluginDeps) {
81
+ if (!dep.includes('/')) {
82
+ log.warn(`Skipping malformed plugin dependency "${dep}" (expected author/name)`);
83
+ continue;
84
+ }
85
+ const [depAuthor, depName] = dep.split('/');
86
+ log.info(`Installing dependent plugin "${dep}" required by ${type} "${pluginName}"...`);
87
+ await installOne('plugin', depAuthor, depName, gitHubToken, installed);
88
+ }
89
+
90
+ // Recursively install dependent evophp themes
91
+ const themeDeps = (deps.theme || '').split(' ').map(d => d.trim()).filter(Boolean);
92
+ for (const dep of themeDeps) {
93
+ if (!dep.includes('/')) {
94
+ log.warn(`Skipping malformed theme dependency "${dep}" (expected author/name)`);
95
+ continue;
96
+ }
97
+ const [depAuthor, depName] = dep.split('/');
98
+ log.info(`Installing dependent theme "${dep}" required by ${type} "${pluginName}"...`);
99
+ await installOne('theme', depAuthor, depName, gitHubToken, installed);
100
+ }
101
+ }
102
+
11
103
  module.exports = {
12
104
  command: 'install [type] [name]',
13
105
  aliases: ['i'],
@@ -36,10 +128,8 @@ module.exports = {
36
128
  let gitHubToken = process.env.GITHUB_TOKEN;
37
129
 
38
130
  if (name.includes("/")) {
39
- // Full author/name provided — no auth required
40
131
  [author, pluginName] = name.split("/");
41
132
  } else {
42
- // Token required to determine user
43
133
  if (!gitHubToken) {
44
134
  gitHubToken = await readLineAsync("Enter your GitHub Personal Access Token: ");
45
135
  saveToEnv("GITHUB_TOKEN", gitHubToken);
@@ -51,6 +141,7 @@ module.exports = {
51
141
  log.info(`Authenticated as ${author}`);
52
142
  }
53
143
 
144
+ // Guard: exit early if the top-level target already exists
54
145
  const paths = type === 'plugin'
55
146
  ? [path.join('Public', 'Modules', pluginName), path.join('EvoJsDev', 'Modules', pluginName)]
56
147
  : [path.join('Public', 'Themes', pluginName)];
@@ -62,24 +153,14 @@ module.exports = {
62
153
  }
63
154
  }
64
155
 
65
- log.info(`Installing ${type} "${pluginName}" from ${author}...`);
66
-
67
- if (type === 'plugin') {
68
- await cloneRepo(`${author}/${pluginName}`, null, paths[0]);
69
- await cloneRepo(`${author}/${pluginName}.js`, null, paths[1]);
156
+ // Shared Set across the entire install tree to prevent duplicate/circular installs
157
+ const installed = new Set();
158
+ await installOne(type, author, pluginName, gitHubToken, installed);
70
159
 
71
- const configFile = path.resolve(`./EvoJsDev/Modules/${pluginName}/config.json`);
72
- if (fs.existsSync(configFile)) {
73
- const pluginConfig = require(configFile);
74
- mergeConfig(pluginConfig);
75
- }
76
-
77
- await installDependencies(paths[1], 'js');
78
- await installDependencies(paths[0], 'php');
160
+ await installDependencies(jsDep, 'js');
161
+ await installDependencies(phpDep, 'php');
79
162
 
80
- } else if (type === 'theme') {
81
- await cloneRepo(`${author}/${pluginName}`, null, paths[0]);
82
- }
163
+ mergeConfig({});
83
164
 
84
165
  log.success(`The ${type} "${pluginName}" has been installed successfully.`);
85
166
 
@@ -5,6 +5,139 @@ const editJsonFile = require("edit-json-file");
5
5
  const { readLineAsync } = require("../utils/helpers");
6
6
  const log = require("../utils/logger");
7
7
 
8
+ /**
9
+ * Returns all installed plugin and theme names as { type, name } objects.
10
+ */
11
+ function getInstalledItems() {
12
+ const items = [];
13
+
14
+ const pluginDir = path.join("Public", "Modules");
15
+ if (fs.existsSync(pluginDir)) {
16
+ fs.readdirSync(pluginDir).forEach(name => {
17
+ if (fs.statSync(path.join(pluginDir, name)).isDirectory()) {
18
+ items.push({ type: "plugin", name });
19
+ }
20
+ });
21
+ }
22
+
23
+ const themeDir = path.join("Public", "Themes");
24
+ if (fs.existsSync(themeDir)) {
25
+ fs.readdirSync(themeDir).forEach(name => {
26
+ if (fs.statSync(path.join(themeDir, name)).isDirectory()) {
27
+ items.push({ type: "theme", name });
28
+ }
29
+ });
30
+ }
31
+
32
+ return items;
33
+ }
34
+
35
+ /**
36
+ * Reads the dependencies.json for a given installed item.
37
+ * Returns parsed object or null if absent/unreadable.
38
+ *
39
+ * @param {'plugin'|'theme'} type
40
+ * @param {string} name
41
+ */
42
+ function readDepsFile(type, name) {
43
+ const depFile = type === "plugin"
44
+ ? path.join("EvoJsDev", "Modules", name, "dependencies.json")
45
+ : path.join("Public", "Themes", name, "dependencies.json");
46
+
47
+ if (!fs.existsSync(depFile)) return null;
48
+
49
+ try {
50
+ return JSON.parse(fs.readFileSync(depFile, "utf-8"));
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Finds all installed plugins/themes that declare a dependency on the target.
58
+ * Returns array of { type, name } of the dependents.
59
+ *
60
+ * @param {'plugin'|'theme'} targetType
61
+ * @param {string} targetName - just the name portion (e.g. "Invoice"), matched
62
+ * against both "name" and "author/name" entries
63
+ */
64
+ function findDependents(targetType, targetName) {
65
+ const dependents = [];
66
+ const depKey = targetType === "plugin" ? "plugin" : "theme";
67
+
68
+ for (const item of getInstalledItems()) {
69
+ // Don't check the item against itself
70
+ if (item.type === targetType && item.name === targetName) continue;
71
+
72
+ const deps = readDepsFile(item.type, item.name);
73
+ if (!deps || !deps[depKey]) continue;
74
+
75
+ const listed = deps[depKey]
76
+ .split(" ")
77
+ .map(d => d.trim())
78
+ .filter(Boolean);
79
+
80
+ const isDependent = listed.some(d => {
81
+ // Match "Invoice" or "kinsokafor/Invoice"
82
+ const parts = d.split("/");
83
+ return parts[parts.length - 1] === targetName;
84
+ });
85
+
86
+ if (isDependent) {
87
+ dependents.push(item);
88
+ }
89
+ }
90
+
91
+ return dependents;
92
+ }
93
+
94
+ /**
95
+ * Uninstalls a single plugin or theme after checking for dependents.
96
+ * Returns true if removed, false if retained due to dependents.
97
+ *
98
+ * @param {'plugin'|'theme'} type
99
+ * @param {string} name
100
+ * @param {boolean} force - skip dependent check (used when caller already confirmed)
101
+ */
102
+ async function uninstallOne(type, name, force = false) {
103
+ const paths = type === "plugin"
104
+ ? [path.join("Public", "Modules", name), path.join("EvoJsDev", "Modules", name)]
105
+ : [path.join("Public", "Themes", name)];
106
+
107
+ const existing = paths.filter(fs.existsSync);
108
+ if (existing.length === 0) {
109
+ log.warn(`${type} "${name}" not found on disk, skipping.`);
110
+ return false;
111
+ }
112
+
113
+ if (!force) {
114
+ const dependents = findDependents(type, name);
115
+ if (dependents.length > 0) {
116
+ const depList = dependents.map(d => `${d.type} "${d.name}"`).join(", ");
117
+ log.warn(`Skipping ${type} "${name}" — still required by: ${depList}`);
118
+ return false;
119
+ }
120
+ }
121
+
122
+ for (const dir of existing) {
123
+ rimraf.sync(dir);
124
+ log.success(`Deleted: ${dir}`);
125
+ }
126
+
127
+ if (type === "plugin") {
128
+ const configPath = path.resolve("config.json");
129
+ if (fs.existsSync(configPath)) {
130
+ let file = editJsonFile(configPath);
131
+ file.unset(`modules.${name}`);
132
+ file.save();
133
+ log.success(`Removed '${name}' from config.json`);
134
+ }
135
+ }
136
+
137
+ log.success(`${type} "${name}" uninstalled.`);
138
+ return true;
139
+ }
140
+
8
141
  module.exports = {
9
142
  command: "uninstall [type] [name]",
10
143
  aliases: ["u"],
@@ -36,13 +169,11 @@ module.exports = {
36
169
  }
37
170
  }
38
171
 
39
- const paths =
40
- type === "plugin"
41
- ? [path.join("Public", "Modules", name), path.join("EvoJsDev", "Modules", name)]
42
- : [path.join("Public", "Themes", name)];
172
+ const paths = type === "plugin"
173
+ ? [path.join("Public", "Modules", name), path.join("EvoJsDev", "Modules", name)]
174
+ : [path.join("Public", "Themes", name)];
43
175
 
44
176
  const existing = paths.filter(fs.existsSync);
45
-
46
177
  if (existing.length === 0) {
47
178
  log.warn(`No matching ${type} found for "${name}".`);
48
179
  process.exit(0);
@@ -57,18 +188,27 @@ module.exports = {
57
188
  return;
58
189
  }
59
190
 
60
- for (const dir of existing) {
61
- rimraf.sync(dir);
62
- log.success(`Deleted: ${dir}`);
63
- }
191
+ // Remove the main target
192
+ await uninstallOne(type, name, true);
193
+
194
+ // Now attempt to remove its evophp dependencies, retaining any still used elsewhere
195
+ const deps = readDepsFile(type, name);
196
+ if (deps) {
197
+ const pluginDeps = (deps.plugin || "").split(" ").map(d => d.trim()).filter(Boolean);
198
+ const themeDeps = (deps.theme || "").split(" ").map(d => d.trim()).filter(Boolean);
199
+
200
+ if (pluginDeps.length > 0 || themeDeps.length > 0) {
201
+ log.info(`Checking evophp dependencies of "${name}" for removal...`);
202
+ }
203
+
204
+ for (const dep of pluginDeps) {
205
+ const depName = dep.includes("/") ? dep.split("/").pop() : dep;
206
+ await uninstallOne("plugin", depName);
207
+ }
64
208
 
65
- if (type === "plugin") {
66
- const configPath = path.resolve("config.json");
67
- if (fs.existsSync(configPath)) {
68
- let file = editJsonFile(configPath);
69
- file.unset(`modules.${name}`);
70
- file.save();
71
- log.success(`Removed '${name}' from config.json`);
209
+ for (const dep of themeDeps) {
210
+ const depName = dep.includes("/") ? dep.split("/").pop() : dep;
211
+ await uninstallOne("theme", depName);
72
212
  }
73
213
  }
74
214
 
@@ -78,4 +218,4 @@ module.exports = {
78
218
  process.exit(1);
79
219
  }
80
220
  }
81
- };
221
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evophp",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "Command-line installer for the Evolution PHP framework",
5
5
  "bin": {
6
6
  "evophp": "bin/evophp.js"
@@ -72,7 +72,8 @@
72
72
  "protocol": "http"
73
73
  }
74
74
  ],
75
- "publickey": "<publickey>"
75
+ "publickey": "<publickey>",
76
+ "sessionSecret": "<sessionSecret>"
76
77
  },
77
78
  "site_name": "<projectName>",
78
79
  "loginLink": "/accounts",
@@ -3,31 +3,59 @@ const path = require('path');
3
3
  const { execSync } = require('child_process');
4
4
  const log = require('./logger');
5
5
 
6
- async function installDependencies(dir, type) {
7
- const dependenciesFile = path.join(dir, 'dependencies.json');
8
- if (!fs.existsSync(dependenciesFile)) {
9
- log.info(`No ${type} dependencies.json found in ${dir}`);
10
- return;
11
- }
12
-
13
- const deps = JSON.parse(fs.readFileSync(dependenciesFile, 'utf-8'));
6
+ async function installDependencies(deps, type) {
14
7
  const cwd = process.cwd();
15
8
 
16
9
  if (type === 'js') {
17
- if (deps.prod) {
10
+ if (deps.prod && (deps.prod || "").trim() != "") {
18
11
  log.info("Installing JS production dependencies...");
19
12
  execSync(`npm install ${deps.prod}`, { cwd, stdio: 'inherit' });
20
13
  }
21
- if (deps.dev) {
14
+ if (deps.dev && (deps.dev || "").trim() != "") {
22
15
  log.info("Installing JS development dependencies...");
23
16
  execSync(`npm install ${deps.dev} --save-dev`, { cwd, stdio: 'inherit' });
24
17
  }
25
18
  }
26
19
 
27
- if (type === 'php' && deps.prod) {
20
+ if (type === 'php' && deps.prod && (deps.prod || "").trim() != "") {
28
21
  log.info("Installing PHP dependencies...");
29
22
  execSync(`composer require ${deps.prod}`, { cwd, stdio: 'inherit' });
30
23
  }
31
24
  }
32
25
 
33
- module.exports = { installDependencies };
26
+ async function extractDependencies(dir, type)
27
+ {
28
+ const dependenciesFile = path.join(dir, 'dependencies.json');
29
+ if (!fs.existsSync(dependenciesFile)) {
30
+ log.info(`No ${type} dependencies.json found in ${dir}`);
31
+ return;
32
+ }
33
+
34
+ const deps = JSON.parse(fs.readFileSync(dependenciesFile, 'utf-8'));
35
+
36
+ return deps;
37
+ }
38
+
39
+ async function mergeDependencies(newDep, oldDep) {
40
+ const res = await new Promise((resolve) => {
41
+ for (const key in newDep) {
42
+ if (oldDep[key]) {
43
+ oldDep[key] = [
44
+ ...new Set(
45
+ `${oldDep[key]} ${newDep[key]}`
46
+ .trim()
47
+ .split(/\s+/)
48
+ .filter(Boolean)
49
+ )
50
+ ].join(" ");
51
+ } else {
52
+ oldDep[key] = String(newDep[key]).trim();
53
+ }
54
+ }
55
+ resolve(oldDep);
56
+ });
57
+
58
+ return res;
59
+ }
60
+
61
+ module.exports = { installDependencies, extractDependencies, mergeDependencies };
package/utils/helpers.js CHANGED
@@ -67,7 +67,7 @@ const mergeDeep = async (target, source) => {
67
67
  return target;
68
68
  };
69
69
 
70
- const mergeConfig = async (config, filePath = "./config.json") => {
70
+ const mergeConfig = async (config, filePath = "./config.json", runConfig = true) => {
71
71
  const file = editJsonFile(filePath);
72
72
  for (const key in config) {
73
73
  const existing = file.get(key);
@@ -83,12 +83,14 @@ const mergeConfig = async (config, filePath = "./config.json") => {
83
83
  }
84
84
  file.save();
85
85
 
86
- // Run build process if entry config changes
87
- try {
88
- console.log("\n[INFO] Running npm run dev to recompile JS bundle...");
89
- execSync("npm run dev", { stdio: "inherit" });
90
- } catch (error) {
91
- console.error("[ERROR] Failed to run 'npm run dev':", error.message);
86
+ if(runConfig) {
87
+ // Run build process if entry config changes
88
+ try {
89
+ console.log("\n[INFO] Running npm run dev to recompile JS bundle...");
90
+ execSync("npm run dev", { stdio: "inherit" });
91
+ } catch (error) {
92
+ console.error("[ERROR] Failed to run 'npm run dev':", error.message);
93
+ }
92
94
  }
93
95
  };
94
96