evophp 1.0.31 → 1.1.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.
@@ -0,0 +1,161 @@
1
+ const path = require("path");
2
+ const fs = require("fs");
3
+ const { readLineAsync, mergeConfig } = require("../utils/helpers");
4
+ const { getUserData, createGitHubRepo } = require("../utils/github");
5
+ const { cloneRepo } = require("../utils/git");
6
+ const { editFile, renameFile } = require("../utils/edit");
7
+ const { test } = require("../utils/validate");
8
+ const log = require("../utils/logger");
9
+ require("dotenv").config({ path: "./.env.development" });
10
+
11
+ module.exports = {
12
+ command: "new <type> <name>",
13
+ aliases: ["n"],
14
+ describe: "Create a new plugin or theme from template",
15
+
16
+ builder: (yargs) =>
17
+ yargs
18
+ .positional("type", {
19
+ describe: "plugin or theme",
20
+ type: "string",
21
+ })
22
+ .positional("name", {
23
+ describe: "The plugin or theme name (without author)",
24
+ type: "string",
25
+ }),
26
+
27
+ handler: async ({ type, name }) => {
28
+ try {
29
+ let gitHubToken = process.env.GITHUB_TOKEN;
30
+
31
+ if (!gitHubToken) {
32
+ gitHubToken = await readLineAsync(
33
+ "Enter GitHub Personal Access Token: "
34
+ );
35
+ const { saveToEnv } = require("../utils/helpers");
36
+ saveToEnv("GITHUB_TOKEN", gitHubToken);
37
+ log.success("GitHub token saved to .env.development");
38
+ }
39
+ type = type.toLowerCase();
40
+
41
+ if (!["plugin", "theme"].includes(type)) {
42
+ log.error("Invalid type. Must be plugin or theme.");
43
+ process.exit(1);
44
+ }
45
+
46
+ // Get authenticated username from token
47
+ const userData = await getUserData(gitHubToken);
48
+ const author = userData.login;
49
+ const pluginName = name;
50
+
51
+ log.info(`Authenticated as ${author}`);
52
+ log.info(`Creating ${type} "${pluginName}"...`);
53
+
54
+ if (type === "plugin") {
55
+ const entry = await setupPlugin(gitHubToken, author, pluginName);
56
+ log.success(`Plugin "${pluginName}" created with entry "${entry}"`);
57
+ } else {
58
+ await setupTheme(gitHubToken, author, pluginName);
59
+ log.success(`Theme "${pluginName}" created`);
60
+ }
61
+ } catch (err) {
62
+ log.error(`Creation failed: ${err.message}`);
63
+ process.exit(1);
64
+ }
65
+ },
66
+ };
67
+
68
+ async function setupPlugin(token, author, pluginName) {
69
+ const entry = await setupPluginRepo(token, author, pluginName);
70
+ const pluginPrefix = entry.replace("Main", "");
71
+
72
+ await editFile(`./Public/Modules/${pluginName}/Routes.php`, {
73
+ pluginName,
74
+ entry,
75
+ entryURI: entry.toLowerCase(),
76
+ pluginPrefix,
77
+ });
78
+
79
+ const controllerPath = `./Public/Modules/${pluginName}/Controller.php`;
80
+ if (fs.existsSync(controllerPath)) {
81
+ await editFile(controllerPath, { pluginName, pluginPrefix });
82
+ await renameFile(
83
+ controllerPath,
84
+ `./Public/Modules/${pluginName}/${pluginPrefix}Controller.php`
85
+ );
86
+ await renameFile(
87
+ `./Public/Modules/${pluginName}/Views/Entry`,
88
+ `./Public/Modules/${pluginName}/Views/${entry}`
89
+ );
90
+ }
91
+
92
+ return entry;
93
+ }
94
+
95
+ async function setupPluginRepo(token, author, pluginName) {
96
+ await createGitHubRepo(token, "kinsokafor/EvoPlugin", pluginName);
97
+ await cloneRepo(
98
+ author,
99
+ pluginName,
100
+ path.join("Public", "Modules", pluginName)
101
+ );
102
+
103
+ await createGitHubRepo(token, "kinsokafor/EvoPlugin.js", `${pluginName}.js`);
104
+ await cloneRepo(
105
+ author,
106
+ `${pluginName}.js`,
107
+ path.join("EvoJsDev", "Modules", pluginName)
108
+ );
109
+
110
+ const pluginPrefix = await getValidatedInput("Enter Plugin Prefix?", 3, true);
111
+ const entryURI = await getValidatedInput("Entry URI?", 20);
112
+ const entry = pluginPrefix + "Main";
113
+
114
+ await editFile(`./EvoJsDev/Modules/${pluginName}/config.json`, {
115
+ pluginName,
116
+ entry,
117
+ });
118
+ await renameFile(
119
+ `./EvoJsDev/Modules/${pluginName}/Entry`,
120
+ `./EvoJsDev/Modules/${pluginName}/${entry}`
121
+ );
122
+
123
+ const configPath = `./EvoJsDev/Modules/${pluginName}/config.json`;
124
+ if (fs.existsSync(configPath)) {
125
+ const pluginConfig = require(path.resolve(configPath));
126
+ mergeConfig(pluginConfig);
127
+ }
128
+
129
+ return entry;
130
+ }
131
+
132
+ async function setupTheme(token, author, pluginName) {
133
+ await createGitHubRepo(token, "kinsokafor/EvoTheme", pluginName);
134
+ await cloneRepo(
135
+ author,
136
+ pluginName,
137
+ path.join("Public", "Themes", pluginName)
138
+ );
139
+
140
+ await editFile(`./Public/Themes/${pluginName}/Data/index.data.php`, {
141
+ pluginName,
142
+ });
143
+ await editFile(`./Public/Themes/${pluginName}/Data/login.data.php`, {
144
+ pluginName,
145
+ });
146
+ await editFile(`./Public/Themes/${pluginName}/Views/index.blade.php`, {
147
+ pluginName,
148
+ });
149
+ await editFile(`./Public/Themes/${pluginName}/Views/login.blade.php`, {
150
+ pluginName,
151
+ });
152
+ }
153
+
154
+ async function getValidatedInput(label, maxLength, caps = false) {
155
+ let input = await readLineAsync(label);
156
+ while (!test(input, maxLength)) {
157
+ input = await readLineAsync(label);
158
+ }
159
+ input = input.trim().replaceAll(" ", "");
160
+ return caps ? input.toUpperCase() : input.toLowerCase();
161
+ }
@@ -0,0 +1,81 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const rimraf = require("rimraf");
4
+ const editJsonFile = require("edit-json-file");
5
+ const { readLineAsync } = require("../utils/helpers");
6
+ const log = require("../utils/logger");
7
+
8
+ module.exports = {
9
+ command: "uninstall [type] [name]",
10
+ aliases: ["u"],
11
+ describe: "Uninstall a plugin or theme",
12
+
13
+ builder: yargs =>
14
+ yargs
15
+ .positional("type", { describe: "plugin or theme", type: "string" })
16
+ .positional("name", { describe: "The plugin or theme name", type: "string" }),
17
+
18
+ handler: async ({ type, name }) => {
19
+ try {
20
+ // Prompt type if missing
21
+ if (!type || !["plugin", "theme"].includes(type.toLowerCase())) {
22
+ type = await readLineAsync("What are you uninstalling? (plugin/theme): ");
23
+ type = type.toLowerCase();
24
+ if (!["plugin", "theme"].includes(type)) {
25
+ log.error("Invalid type. Must be 'plugin' or 'theme'.");
26
+ process.exit(1);
27
+ }
28
+ }
29
+
30
+ // Prompt name if missing
31
+ if (!name) {
32
+ name = await readLineAsync(`Enter the ${type} name (e.g., example${type}): `);
33
+ if (!name) {
34
+ log.error("You must provide a name.");
35
+ process.exit(1);
36
+ }
37
+ }
38
+
39
+ const paths =
40
+ type === "plugin"
41
+ ? [path.join("Public", "Modules", name), path.join("EvoJsDev", "Modules", name)]
42
+ : [path.join("Public", "Themes", name)];
43
+
44
+ const existing = paths.filter(fs.existsSync);
45
+
46
+ if (existing.length === 0) {
47
+ log.warn(`No matching ${type} found for "${name}".`);
48
+ process.exit(0);
49
+ }
50
+
51
+ log.info(`Preparing to uninstall ${type} "${name}" from:`);
52
+ existing.forEach(p => log.info(" - " + p));
53
+
54
+ const confirm = await readLineAsync("Are you sure you want to proceed? (y/n): ");
55
+ if (!["y", "yes"].includes(confirm.trim().toLowerCase())) {
56
+ log.warn("Uninstallation cancelled.");
57
+ return;
58
+ }
59
+
60
+ for (const dir of existing) {
61
+ rimraf.sync(dir);
62
+ log.success(`Deleted: ${dir}`);
63
+ }
64
+
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`);
72
+ }
73
+ }
74
+
75
+ log.success(`The ${type} "${name}" has been uninstalled successfully.`);
76
+ } catch (err) {
77
+ log.error("Error during uninstall: " + err.message);
78
+ process.exit(1);
79
+ }
80
+ }
81
+ };
package/package.json CHANGED
@@ -1,26 +1,33 @@
1
1
  {
2
2
  "name": "evophp",
3
- "version": "1.0.31",
4
- "description": "Installer for the Evolution PHP project",
5
- "main": "installer.js",
6
- "scripts": {
7
- "start": "node installer.js"
8
- },
3
+ "version": "1.1.01",
4
+ "description": "Command-line installer for the Evolution PHP framework",
9
5
  "bin": {
10
- "evophp": "installer.js"
6
+ "evophp": "./bin/evophp.js"
7
+ },
8
+ "main": "bin/evophp.js",
9
+ "scripts": {
10
+ "start": "node bin/evophp.js"
11
11
  },
12
+ "files": [
13
+ "bin/",
14
+ "commands/",
15
+ "utils/"
16
+ ],
12
17
  "dependencies": {
13
18
  "axios": "^1.7.9",
14
19
  "dotenv": "^16.4.7",
15
20
  "edit-json-file": "^1.8.0",
16
- "rimraf": "^6.0.1"
21
+ "rimraf": "^6.0.1",
22
+ "yargs": "^17.7.2"
17
23
  },
18
- "files": [
19
- "installer.js",
20
- "new-plugin.js",
21
- "install-plugin.js",
22
- "install-dependencies.js",
23
- "helpers.js",
24
- "uninstall.js"
25
- ]
26
- }
24
+ "keywords": [
25
+ "cli",
26
+ "evophp",
27
+ "installer",
28
+ "php-framework",
29
+ "evolution"
30
+ ],
31
+ "author": "Kingsley Okafor",
32
+ "license": "MIT"
33
+ }
@@ -0,0 +1,20 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const configPath = path.join(require('os').homedir(), '.evophp-cli.json');
4
+
5
+ function loadConfig() {
6
+ if (fs.existsSync(configPath)) {
7
+ return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
8
+ }
9
+ return {};
10
+ }
11
+
12
+ function saveConfig(data) {
13
+ fs.writeFileSync(configPath, JSON.stringify(data, null, 2));
14
+ }
15
+
16
+ module.exports = {
17
+ loadConfig,
18
+ saveConfig,
19
+ configPath
20
+ };
@@ -0,0 +1,33 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+ const log = require('./logger');
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'));
14
+ const cwd = process.cwd();
15
+
16
+ if (type === 'js') {
17
+ if (deps.prod) {
18
+ log.info("Installing JS production dependencies...");
19
+ execSync(`npm install ${deps.prod}`, { cwd, stdio: 'inherit' });
20
+ }
21
+ if (deps.dev) {
22
+ log.info("Installing JS development dependencies...");
23
+ execSync(`npm install ${deps.dev} --save-dev`, { cwd, stdio: 'inherit' });
24
+ }
25
+ }
26
+
27
+ if (type === 'php' && deps.prod) {
28
+ log.info("Installing PHP dependencies...");
29
+ execSync(`composer require ${deps.prod}`, { cwd, stdio: 'inherit' });
30
+ }
31
+ }
32
+
33
+ module.exports = { installDependencies };
package/utils/edit.js ADDED
@@ -0,0 +1,42 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const log = require("./logger");
4
+
5
+ async function editFile(file, data) {
6
+ return new Promise((resolve, reject) => {
7
+ fs.readFile(file, "utf8", (err, fileData) => {
8
+ if (err) {
9
+ log.error(`Failed to read file ${file}`);
10
+ return reject(err);
11
+ }
12
+
13
+ for (const key in data) {
14
+ fileData = fileData.replaceAll(`<${key}>`, data[key]);
15
+ }
16
+
17
+ fs.writeFile(file, fileData, "utf8", err => {
18
+ if (err) {
19
+ log.error(`Failed to write file ${file}`);
20
+ return reject(err);
21
+ }
22
+ log.success(`${file} updated.`);
23
+ resolve(file);
24
+ });
25
+ });
26
+ });
27
+ }
28
+
29
+ async function renameFile(oldPath, newPath) {
30
+ return new Promise((resolve, reject) => {
31
+ fs.rename(oldPath, newPath, err => {
32
+ if (err) {
33
+ log.error(`Failed to rename ${oldPath}`);
34
+ return reject(err);
35
+ }
36
+ log.success(`Renamed: ${path.basename(oldPath)} → ${path.basename(newPath)}`);
37
+ resolve();
38
+ });
39
+ });
40
+ }
41
+
42
+ module.exports = { editFile, renameFile };
package/utils/git.js ADDED
@@ -0,0 +1,46 @@
1
+ const { execSync } = require("child_process");
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const rimraf = require("rimraf");
5
+ const log = require("./logger");
6
+
7
+ /**
8
+ * Clone a GitHub repo into a destination folder.
9
+ * Supports:
10
+ * cloneRepo("author", "repo", "dest")
11
+ * cloneRepo("author/repo", null, "dest")
12
+ *
13
+ * @param {string} authorOrFull - GitHub username OR "author/repo"
14
+ * @param {string|null} repo - Optional repo name if not included in first arg
15
+ * @param {string} dest - Local destination path
16
+ */
17
+ async function cloneRepo(authorOrFull, repo, dest) {
18
+ let author = "";
19
+ let repoName = "";
20
+
21
+ if (repo === null || repo === undefined) {
22
+ // authorOrFull is in "author/repo" format
23
+ const parts = authorOrFull.split("/");
24
+ if (parts.length !== 2) {
25
+ log.error("Invalid GitHub repo format. Use 'author/repo'.");
26
+ process.exit(1);
27
+ }
28
+ [author, repoName] = parts;
29
+ } else {
30
+ // Passed separately
31
+ author = authorOrFull;
32
+ repoName = repo;
33
+ }
34
+
35
+ const cloneUrl = `https://github.com/${author}/${repoName}.git`;
36
+
37
+ if (fs.existsSync(dest)) {
38
+ log.warn(`Deleting existing folder: ${dest}`);
39
+ rimraf.sync(dest);
40
+ }
41
+
42
+ log.info(`Cloning ${author}/${repoName} → ${dest}`);
43
+ execSync(`git clone ${cloneUrl} ${dest}`, { stdio: "inherit" });
44
+ }
45
+
46
+ module.exports = { cloneRepo };
@@ -0,0 +1,49 @@
1
+ // utils/github.js
2
+ const axios = require("axios");
3
+ const log = require("./logger");
4
+
5
+ async function getUserData(token) {
6
+ try {
7
+ const res = await axios.get("https://api.github.com/user", {
8
+ headers: {
9
+ Authorization: `token ${token}`,
10
+ Accept: "application/vnd.github.v3+json",
11
+ },
12
+ });
13
+ return res.data;
14
+ } catch (e) {
15
+ log.error("Failed to fetch GitHub user info.");
16
+ throw e;
17
+ }
18
+ }
19
+
20
+ async function createGitHubRepo(token, templateRepo, pluginName) {
21
+ try {
22
+ const res = await axios.post(
23
+ `https://api.github.com/repos/${templateRepo}/generate`,
24
+ {
25
+ name: pluginName,
26
+ private: true,
27
+ description: `A new ${templateRepo.split("/")[1]} created from template.`,
28
+ },
29
+ {
30
+ headers: {
31
+ Authorization: `token ${token}`,
32
+ Accept: "application/vnd.github.v3+json",
33
+ },
34
+ }
35
+ );
36
+
37
+ if (res.status === 201) {
38
+ log.success(`Created repo ${pluginName} from ${templateRepo}`);
39
+ } else {
40
+ log.error("Failed to create repo:", res.data);
41
+ process.exit(1);
42
+ }
43
+ } catch (e) {
44
+ log.error(`GitHub repo creation failed: ${e.message}`);
45
+ process.exit(1);
46
+ }
47
+ }
48
+
49
+ module.exports = { getUserData, createGitHubRepo };
@@ -0,0 +1,161 @@
1
+ const readline = require("readline");
2
+ const editJsonFile = require("edit-json-file");
3
+
4
+ /**
5
+ * Prompt user for input with a message
6
+ * @param {string} msg - The prompt message to display
7
+ * @returns {Promise<string>}
8
+ */
9
+ const readLineAsync = (msg) => {
10
+ const rl = readline.createInterface({
11
+ input: process.stdin,
12
+ output: process.stdout
13
+ });
14
+
15
+ return new Promise((resolve) => {
16
+ rl.question(msg, (userRes) => {
17
+ resolve(userRes.trim());
18
+ rl.close();
19
+ });
20
+ });
21
+ };
22
+
23
+ /**
24
+ * Validate and split a name into [author, pluginName]
25
+ * Accepts "author/plugin" or a GitHub URL
26
+ * @param {string} name
27
+ * @returns {Promise<[string, string]>}
28
+ */
29
+ const splitPluginName = async (name) => {
30
+ const isValid = (input) =>
31
+ /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(input);
32
+
33
+ while (true) {
34
+ name = name.replace(/^(https?:\/\/)?(www\.)?github\.com\//, "").trim();
35
+ if (isValid(name)) return name.split("/");
36
+
37
+ console.log("Format must be: author/plugin-name");
38
+ name = await readLineAsync("Enter the plugin name (or 'cancel' to exit): ");
39
+ if (name.toLowerCase() === "cancel") {
40
+ console.log("Process canceled by user.");
41
+ process.exit(1);
42
+ }
43
+ }
44
+ };
45
+
46
+ /**
47
+ * Check if value is a plain object
48
+ */
49
+ const isObject = (item) =>
50
+ item && typeof item === "object" && !Array.isArray(item);
51
+
52
+ /**
53
+ * Deep merge multiple objects into one
54
+ */
55
+ const mergeDeep = async (target, ...sources) => {
56
+ if (!sources.length) return target;
57
+ const source = sources.shift();
58
+
59
+ if (isObject(target) && isObject(source)) {
60
+ for (const key in source) {
61
+ if (isObject(source[key])) {
62
+ if (!target[key]) Object.assign(target, { [key]: {} });
63
+ await mergeDeep(target[key], source[key]);
64
+ } else {
65
+ Object.assign(target, { [key]: source[key] });
66
+ }
67
+ }
68
+ }
69
+
70
+ return mergeDeep(target, ...sources);
71
+ };
72
+
73
+ /**
74
+ * Merge plugin config into config.json
75
+ */
76
+ const mergeConfig = async (config) => {
77
+ const file = editJsonFile("./config.json");
78
+
79
+ for (const key in config) {
80
+ const existing = file.get(key);
81
+ if (existing === undefined) {
82
+ file.set(key, config[key]);
83
+ } else if (isObject(existing)) {
84
+ const merged = await mergeDeep(existing, config[key]);
85
+ file.set(key, merged);
86
+ } else {
87
+ file.set(key, config[key]);
88
+ }
89
+ }
90
+
91
+ file.save();
92
+ };
93
+
94
+ /**
95
+ * Add a new entry point to a plugin's config.json file
96
+ * @param {string} pluginName - The plugin name
97
+ * @param {string} entryName - The entry directory name
98
+ */
99
+ function addPluginEntry(pluginName, entryName) {
100
+ const configPath = path.join("EvoJsDev", "Modules", pluginName, "config.json");
101
+
102
+ if (!fs.existsSync(configPath)) {
103
+ log.error(`Config file not found: ${configPath}`);
104
+ return;
105
+ }
106
+
107
+ const file = editJsonFile(configPath);
108
+ const entryPath = `${entryName}/index`;
109
+
110
+ const current = file.get(`modules.${pluginName}`) || {
111
+ entry: [],
112
+ active: true,
113
+ order: 0
114
+ };
115
+
116
+ if (!current.entry.includes(entryPath)) {
117
+ current.entry.push(entryPath);
118
+ }
119
+
120
+ file.set(`modules.${pluginName}`, current);
121
+ file.save();
122
+ log.success(`Added entry "${entryPath}" to ${pluginName} config.`);
123
+ }
124
+
125
+ const fs = require("fs");
126
+ const path = require("path");
127
+
128
+ /**
129
+ * Save a key-value pair to .env.development (creates or updates)
130
+ * @param {string} key
131
+ * @param {string} value
132
+ */
133
+ function saveToEnv(key, value) {
134
+ const envPath = path.resolve(".env.development");
135
+
136
+ let env = "";
137
+ if (fs.existsSync(envPath)) {
138
+ env = fs.readFileSync(envPath, "utf-8");
139
+ }
140
+
141
+ const regex = new RegExp(`^${key}=.*$`, "m");
142
+ const line = `${key}=${value.trim()}`;
143
+
144
+ if (regex.test(env)) {
145
+ // Update existing key
146
+ env = env.replace(regex, line);
147
+ } else {
148
+ // Append new key
149
+ env += (env.endsWith("\n") ? "" : "\n") + line + "\n";
150
+ }
151
+
152
+ fs.writeFileSync(envPath, env, "utf-8");
153
+ }
154
+
155
+ module.exports = {
156
+ readLineAsync,
157
+ splitPluginName,
158
+ mergeConfig,
159
+ addPluginEntry,
160
+ saveToEnv
161
+ };
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ info: msg => console.log(`[INFO] ${msg}`),
3
+ error: msg => console.error(`[ERROR] ${msg}`),
4
+ success: msg => console.log(`[✓] ${msg}`),
5
+ warn: msg => console.warn(`[WARN] ${msg}`)
6
+ };
@@ -0,0 +1,16 @@
1
+ const log = require("./logger");
2
+
3
+ function test(str, maxLength) {
4
+ str = str.trim().replaceAll(" ", "");
5
+ if (str.length > maxLength) {
6
+ log.error(`Input can't exceed ${maxLength} characters.`);
7
+ return false;
8
+ }
9
+ if (str.length < 1) {
10
+ log.error(`Input is empty.`);
11
+ return false;
12
+ }
13
+ return str;
14
+ }
15
+
16
+ module.exports = { test };