@ijuantm/simpl-addon 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/install.js +349 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Simpl Add-on Installer
|
|
2
|
+
|
|
3
|
+
CLI tool for installing Simpl framework add-ons automatically using npx.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
### List Available Add-ons
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @ijuantm/simpl-addon --list
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Install an Add-on
|
|
14
|
+
|
|
15
|
+
Navigate to your Simpl project directory and run, for example, to install the "auth" add-on:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx @ijuantm/simpl-addon auth
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The installer will:
|
|
22
|
+
|
|
23
|
+
1. Download the add-on
|
|
24
|
+
2. Copy new files to your project
|
|
25
|
+
3. Merge existing files using markers
|
|
26
|
+
|
|
27
|
+
### Get Help
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx @ijuantm/simpl-addon --help
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## How It Works
|
|
34
|
+
|
|
35
|
+
The installer uses special markers in add-on files to safely merge content:
|
|
36
|
+
|
|
37
|
+
```php
|
|
38
|
+
// @addon-insert:after('existing line')
|
|
39
|
+
new AuthController();
|
|
40
|
+
// @addon-end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Supported Markers:**
|
|
44
|
+
|
|
45
|
+
- `@addon-insert:after('text')` - Insert content after matching line
|
|
46
|
+
- `@addon-insert:before('text')` - Insert content before matching line
|
|
47
|
+
- `@addon-insert:prepend` - Add content at the beginning of the file
|
|
48
|
+
- `@addon-insert:append` - Add content at the end of the file
|
|
49
|
+
|
|
50
|
+
The installer:
|
|
51
|
+
|
|
52
|
+
- Creates new files that don't exist
|
|
53
|
+
- Merges files with markers automatically
|
|
54
|
+
- Skips files without markers (no overwriting)
|
|
55
|
+
- Detects duplicate content (won't add twice)
|
|
56
|
+
|
|
57
|
+
## Requirements
|
|
58
|
+
|
|
59
|
+
- **Node.js**: >= 20.x.x
|
|
60
|
+
- **Simpl Framework**: A (preferably clean) installation of Simpl, if not clean, some manual merging may be required, or the installer may skip files or break things (you have been warned).
|
package/install.js
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const https = require('https');
|
|
6
|
+
|
|
7
|
+
const COLORS = {
|
|
8
|
+
reset: '\x1b[0m', green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', cyan: '\x1b[36m', blue: '\x1b[34m', gray: '\x1b[90m', bold: '\x1b[1m', dim: '\x1b[2m'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const BRANCH = 'master';
|
|
12
|
+
const REPO_BASE = 'https://api.github.com/repos/IJuanTM/simpl/contents/add-ons';
|
|
13
|
+
const RAW_BASE = `https://raw.githubusercontent.com/IJuanTM/simpl/${BRANCH}/add-ons`;
|
|
14
|
+
|
|
15
|
+
const log = (message, color = 'reset') => console.log(`${COLORS[color]}${message}${COLORS.reset}`);
|
|
16
|
+
|
|
17
|
+
const fetchUrl = (url) => new Promise((resolve, reject) => {
|
|
18
|
+
const headers = {'User-Agent': 'simpl-installer'};
|
|
19
|
+
if (process.env.GITHUB_TOKEN) headers['Authorization'] = `Bearer ${process.env.GITHUB_TOKEN}`;
|
|
20
|
+
|
|
21
|
+
https.get(url, {headers}, res => {
|
|
22
|
+
if (res.statusCode === 302 || res.statusCode === 301) return fetchUrl(res.headers.location).then(resolve).catch(reject);
|
|
23
|
+
if (res.statusCode === 403) {
|
|
24
|
+
const resetTime = res.headers['x-ratelimit-reset'];
|
|
25
|
+
const resetDate = resetTime ? new Date(resetTime * 1000).toLocaleTimeString() : 'unknown';
|
|
26
|
+
return reject(new Error(`GitHub API rate limit exceeded. Resets at ${resetDate}. ${process.env.GITHUB_TOKEN ? 'Token is set but may be invalid.' : 'Set GITHUB_TOKEN environment variable to increase limit.'}`));
|
|
27
|
+
}
|
|
28
|
+
if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage || 'Request failed'}`));
|
|
29
|
+
|
|
30
|
+
let data = '';
|
|
31
|
+
res.on('data', chunk => data += chunk);
|
|
32
|
+
res.on('end', () => resolve(data));
|
|
33
|
+
}).on('error', reject);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const showHelp = () => {
|
|
37
|
+
console.log();
|
|
38
|
+
log(` ╭${'─'.repeat(62)}╮`);
|
|
39
|
+
log(` │ ${COLORS.bold}Simpl Add-on Installer${COLORS.reset}${' '.repeat(38)}│`);
|
|
40
|
+
log(` ╰${'─'.repeat(62)}╯`);
|
|
41
|
+
console.log();
|
|
42
|
+
log(' Usage:', 'cyan');
|
|
43
|
+
log(' npx @ijuantm/simpl-addon <addon-name>');
|
|
44
|
+
log(' npx @ijuantm/simpl-addon --list');
|
|
45
|
+
log(' npx @ijuantm/simpl-addon --help');
|
|
46
|
+
console.log();
|
|
47
|
+
log(' Commands:', 'cyan');
|
|
48
|
+
log(' <addon-name> Install the specified add-on');
|
|
49
|
+
log(' --list, -l List all available add-ons');
|
|
50
|
+
log(' --help, -h Show this help message');
|
|
51
|
+
console.log();
|
|
52
|
+
log(' Examples:', 'cyan');
|
|
53
|
+
log(' npx @ijuantm/simpl-addon auth');
|
|
54
|
+
log(' npx @ijuantm/simpl-addon --list');
|
|
55
|
+
console.log();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const listAddons = async () => {
|
|
59
|
+
console.log();
|
|
60
|
+
log(` ╭${'─'.repeat(62)}╮`);
|
|
61
|
+
log(` │ ${COLORS.bold}Available Add-ons${COLORS.reset}${' '.repeat(43)}│`);
|
|
62
|
+
log(` ╰${'─'.repeat(62)}╯`);
|
|
63
|
+
console.log();
|
|
64
|
+
log(' 📦 Fetching add-ons from GitHub...', 'bold');
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetchUrl(`${REPO_BASE}?ref=${BRANCH}`);
|
|
68
|
+
const addons = JSON.parse(response).filter(item => item.type === 'dir').map(item => item.name);
|
|
69
|
+
|
|
70
|
+
console.log();
|
|
71
|
+
|
|
72
|
+
if (addons.length === 0) {
|
|
73
|
+
log(` ${COLORS.yellow}⚠${COLORS.reset} No add-ons available`);
|
|
74
|
+
} else {
|
|
75
|
+
addons.forEach(name => log(` ${COLORS.cyan}•${COLORS.reset} ${name}`));
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.log();
|
|
79
|
+
log(` ${COLORS.red}✗${COLORS.reset} Failed to fetch add-ons: ${error.message}`, 'red');
|
|
80
|
+
console.log();
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log();
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const extractMarkers = (content) => {
|
|
88
|
+
const markers = [];
|
|
89
|
+
|
|
90
|
+
content.split('\n').forEach((line, i) => {
|
|
91
|
+
const afterMatch = line.match(/@addon-insert:after\s*\(\s*["'](.+?)["']\s*\)/);
|
|
92
|
+
const beforeMatch = line.match(/@addon-insert:before\s*\(\s*["'](.+?)["']\s*\)/);
|
|
93
|
+
|
|
94
|
+
if (afterMatch) markers.push({type: 'after', lineIndex: i, searchText: afterMatch[1]}); else if (beforeMatch) markers.push({type: 'before', lineIndex: i, searchText: beforeMatch[1]}); else if (line.includes('@addon-insert:prepend')) markers.push({type: 'prepend', lineIndex: i}); else if (line.includes('@addon-insert:append')) markers.push({type: 'append', lineIndex: i});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return markers;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const collectContentBetweenMarkers = (lines, startIndex) => {
|
|
101
|
+
const content = [];
|
|
102
|
+
|
|
103
|
+
for (let i = startIndex + 1; i < lines.length; i++) {
|
|
104
|
+
if (lines[i].trim().includes('@addon-end')) break;
|
|
105
|
+
content.push(lines[i]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return content;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const normalizeContent = (lines) => lines.map(l => l.trim())
|
|
112
|
+
.filter(l => l && !l.startsWith('//') && !l.startsWith('#') && !l.startsWith('/*') && !l.startsWith('*'))
|
|
113
|
+
.join('|');
|
|
114
|
+
|
|
115
|
+
const processEnvContent = (content, targetContent) => {
|
|
116
|
+
const envVarsToAdd = [], comments = [];
|
|
117
|
+
|
|
118
|
+
content.forEach(line => {
|
|
119
|
+
const trimmed = line.trim();
|
|
120
|
+
|
|
121
|
+
if (trimmed.startsWith('#') || !trimmed) {
|
|
122
|
+
comments.push(line);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
|
|
127
|
+
if (match && !new RegExp(`^${match[1]}=`, 'm').test(targetContent)) envVarsToAdd.push(line);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return {content: [...comments, ...envVarsToAdd], count: envVarsToAdd.length};
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const findInsertIndex = (lines, searchText, type) => {
|
|
134
|
+
for (let i = 0; i < lines.length; i++) {
|
|
135
|
+
if (lines[i].includes(searchText)) return type === 'before' ? i : i + 1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return -1;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const mergeFile = (targetPath, addonContent, markers, isEnv = false) => {
|
|
142
|
+
const targetContent = fs.readFileSync(targetPath, 'utf8');
|
|
143
|
+
const addonLines = addonContent.split('\n');
|
|
144
|
+
const operations = [];
|
|
145
|
+
let newContent = targetContent;
|
|
146
|
+
|
|
147
|
+
markers.forEach(marker => {
|
|
148
|
+
let content = collectContentBetweenMarkers(addonLines, marker.lineIndex);
|
|
149
|
+
if (content.length === 0) return;
|
|
150
|
+
|
|
151
|
+
let lineCount = content.length;
|
|
152
|
+
|
|
153
|
+
if (isEnv) {
|
|
154
|
+
const processed = processEnvContent(content, newContent);
|
|
155
|
+
content = processed.content;
|
|
156
|
+
lineCount = processed.count;
|
|
157
|
+
|
|
158
|
+
if (content.length === 0) {
|
|
159
|
+
operations.push({success: false, type: marker.type, lines: 0, searchText: marker.searchText});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
const signature = normalizeContent(content);
|
|
164
|
+
const targetSignature = normalizeContent(newContent.split('\n'));
|
|
165
|
+
|
|
166
|
+
if (signature && targetSignature.includes(signature)) {
|
|
167
|
+
operations.push({success: false, type: marker.type, lines: content.length, searchText: marker.searchText});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (marker.type === 'prepend') {
|
|
173
|
+
newContent = content.join('\n') + '\n' + newContent;
|
|
174
|
+
operations.push({success: true, type: 'prepend', lines: lineCount});
|
|
175
|
+
} else if (marker.type === 'append') {
|
|
176
|
+
if (!newContent.endsWith('\n')) newContent += '\n';
|
|
177
|
+
newContent += '\n' + content.join('\n') + '\n';
|
|
178
|
+
operations.push({success: true, type: 'append', lines: lineCount});
|
|
179
|
+
} else if ((marker.type === 'after' || marker.type === 'before') && marker.searchText) {
|
|
180
|
+
const targetLines = newContent.split('\n');
|
|
181
|
+
const insertIndex = findInsertIndex(targetLines, marker.searchText, marker.type);
|
|
182
|
+
|
|
183
|
+
if (insertIndex === -1) {
|
|
184
|
+
operations.push({success: false, type: 'notfound', searchText: marker.searchText});
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
targetLines.splice(insertIndex, 0, ...content);
|
|
189
|
+
newContent = targetLines.join('\n');
|
|
190
|
+
operations.push({success: true, type: marker.type, lines: lineCount, searchText: marker.searchText});
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (newContent !== targetContent) fs.writeFileSync(targetPath, newContent, 'utf8');
|
|
195
|
+
return {modified: newContent !== targetContent, operations};
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const printMergeResults = (relativePath, isEnv, result) => {
|
|
199
|
+
const indent = ' ';
|
|
200
|
+
const varText = isEnv ? 'environment variable' : 'line';
|
|
201
|
+
let hasChanges = false;
|
|
202
|
+
|
|
203
|
+
result.operations.forEach(op => {
|
|
204
|
+
if (op.success) {
|
|
205
|
+
hasChanges = true;
|
|
206
|
+
|
|
207
|
+
if (op.type === 'prepend') log(`${indent}${COLORS.green}✓${COLORS.reset} Prepended ${COLORS.bold}${op.lines}${COLORS.reset} ${varText}${op.lines !== 1 ? 's' : ''} to file start`); else if (op.type === 'append') log(`${indent}${COLORS.green}✓${COLORS.reset} Appended ${COLORS.bold}${op.lines}${COLORS.reset} ${varText}${op.lines !== 1 ? 's' : ''} to file end`); else if (op.type === 'after') log(`${indent}${COLORS.green}✓${COLORS.reset} Inserted ${COLORS.bold}${op.lines}${COLORS.reset} ${varText}${op.lines !== 1 ? 's' : ''} ${COLORS.cyan}after${COLORS.reset} "${COLORS.dim}${op.searchText}${COLORS.reset}"`); else if (op.type === 'before') log(`${indent}${COLORS.green}✓${COLORS.reset} Inserted ${COLORS.bold}${op.lines}${COLORS.reset} ${varText}${op.lines !== 1 ? 's' : ''} ${COLORS.cyan}before${COLORS.reset} "${COLORS.dim}${op.searchText}${COLORS.reset}"`);
|
|
208
|
+
} else if (op.type === 'notfound') log(`${indent}${COLORS.yellow}⚠${COLORS.reset} ${COLORS.yellow}Could not find target:${COLORS.reset} "${COLORS.dim}${op.searchText}${COLORS.reset}"`); else log(`${indent}${COLORS.gray}○${COLORS.reset} ${COLORS.dim}Content already exists (${op.type})${COLORS.reset}`);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return hasChanges;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const downloadAddonFiles = async (addonName, targetDir) => {
|
|
215
|
+
const addonUrl = `${REPO_BASE}/${addonName}?ref=${BRANCH}`;
|
|
216
|
+
let files;
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
files = JSON.parse(await fetchUrl(addonUrl));
|
|
220
|
+
} catch (error) {
|
|
221
|
+
throw new Error(`Add-on "${addonName}" not found`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const copied = [], skipped = [], toMerge = [];
|
|
225
|
+
|
|
226
|
+
const processFiles = async (fileList, basePath = '') => {
|
|
227
|
+
for (const file of fileList) {
|
|
228
|
+
if (file.name === 'README.md') continue;
|
|
229
|
+
|
|
230
|
+
const relativePath = path.join(basePath, file.name).replace(/\\/g, '/');
|
|
231
|
+
const destPath = path.join(targetDir, relativePath);
|
|
232
|
+
|
|
233
|
+
if (file.type === 'dir') {
|
|
234
|
+
const subUrl = file.url.includes('?') ? `${file.url}&ref=${BRANCH}` : `${file.url}?ref=${BRANCH}`;
|
|
235
|
+
const subFiles = JSON.parse(await fetchUrl(subUrl));
|
|
236
|
+
await processFiles(subFiles, relativePath);
|
|
237
|
+
} else {
|
|
238
|
+
const content = await fetchUrl(`${RAW_BASE}/${addonName}/${relativePath}`);
|
|
239
|
+
|
|
240
|
+
if (fs.existsSync(destPath)) {
|
|
241
|
+
const markers = extractMarkers(content);
|
|
242
|
+
|
|
243
|
+
if (markers.length > 0 || file.name === '.env') toMerge.push({content, destPath, relativePath, markers}); else skipped.push(relativePath);
|
|
244
|
+
} else {
|
|
245
|
+
fs.mkdirSync(path.dirname(destPath), {recursive: true});
|
|
246
|
+
fs.writeFileSync(destPath, content, 'utf8');
|
|
247
|
+
copied.push(relativePath);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
await processFiles(files);
|
|
254
|
+
return {copied, skipped, toMerge};
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const mergeFiles = (toMerge) => {
|
|
258
|
+
if (toMerge.length === 0) return {merged: [], failed: [], unchanged: []};
|
|
259
|
+
|
|
260
|
+
const merged = [], failed = [], unchanged = [];
|
|
261
|
+
|
|
262
|
+
toMerge.forEach(({content, destPath, relativePath, markers}) => {
|
|
263
|
+
const isEnv = path.basename(destPath) === '.env';
|
|
264
|
+
log(`\n ${COLORS.cyan}•${COLORS.reset} ${COLORS.bold}${relativePath}${COLORS.reset}`);
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const result = mergeFile(destPath, content, markers, isEnv);
|
|
268
|
+
if (printMergeResults(relativePath, isEnv, result)) merged.push(relativePath); else unchanged.push(relativePath);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
log(` ${COLORS.red}✗ Error:${COLORS.reset} ${error.message}`, 'red');
|
|
271
|
+
failed.push(relativePath);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return {merged, failed, unchanged};
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const main = async () => {
|
|
279
|
+
const command = process.argv[2];
|
|
280
|
+
|
|
281
|
+
if (!command || command === '--help' || command === '-h') {
|
|
282
|
+
showHelp();
|
|
283
|
+
process.exit(0);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (command === '--list' || command === '-l') {
|
|
287
|
+
await listAddons();
|
|
288
|
+
process.exit(0);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
console.log();
|
|
292
|
+
log(` ╭${'─'.repeat(62)}╮`);
|
|
293
|
+
log(` │ ${COLORS.bold}Installing add-on: ${COLORS.cyan}${command}${COLORS.reset}${' '.repeat(41 - command.length)}│`);
|
|
294
|
+
log(` ╰${'─'.repeat(62)}╯`);
|
|
295
|
+
console.log();
|
|
296
|
+
log(' 📦 Downloading add-on from GitHub...', 'bold');
|
|
297
|
+
|
|
298
|
+
let copied, skipped, toMerge;
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
({copied, skipped, toMerge} = await downloadAddonFiles(command, process.cwd()));
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.log();
|
|
304
|
+
log(` ${COLORS.red}✗${COLORS.reset} ${error.message}`, 'red');
|
|
305
|
+
log(` ${COLORS.dim}Run ${COLORS.cyan}npx @ijuantm/simpl-addon --list${COLORS.reset}${COLORS.dim} to see available add-ons${COLORS.reset}`);
|
|
306
|
+
console.log();
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (copied.length > 0) {
|
|
311
|
+
console.log();
|
|
312
|
+
log(` ${COLORS.green}✓${COLORS.reset} Copied ${COLORS.bold}${copied.length}${COLORS.reset} new file${copied.length !== 1 ? 's' : ''}`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (skipped.length > 0) {
|
|
316
|
+
console.log();
|
|
317
|
+
log(` ${COLORS.gray}○${COLORS.reset} ${COLORS.dim}Skipped ${skipped.length} file${skipped.length !== 1 ? 's' : ''} (no merge markers):${COLORS.reset}`);
|
|
318
|
+
skipped.forEach(file => log(` ${COLORS.dim}• ${file}${COLORS.reset}`));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (toMerge.length > 0) {
|
|
322
|
+
console.log();
|
|
323
|
+
log(' 🔀 Merging existing files...', 'bold');
|
|
324
|
+
const {merged, failed, unchanged} = mergeFiles(toMerge);
|
|
325
|
+
|
|
326
|
+
console.log();
|
|
327
|
+
log(' ' + '─'.repeat(16), 'gray');
|
|
328
|
+
console.log();
|
|
329
|
+
|
|
330
|
+
if (merged.length > 0) log(` ${COLORS.green}✓${COLORS.reset} Successfully merged ${COLORS.bold}${merged.length}${COLORS.reset} file${merged.length !== 1 ? 's' : ''}`);
|
|
331
|
+
if (unchanged.length > 0) log(` ${COLORS.gray}○${COLORS.reset} ${COLORS.dim}${unchanged.length} file${unchanged.length !== 1 ? 's' : ''} unchanged (content already exists)${COLORS.reset}`);
|
|
332
|
+
|
|
333
|
+
if (failed.length > 0) {
|
|
334
|
+
console.log();
|
|
335
|
+
log(` ${COLORS.yellow}⚠${COLORS.reset} ${COLORS.yellow}${failed.length} file${failed.length !== 1 ? 's' : ''} failed to merge${COLORS.reset}`);
|
|
336
|
+
log(` ${COLORS.yellow}Please review manually:${COLORS.reset}`);
|
|
337
|
+
failed.forEach(file => log(` ${COLORS.cyan}• ${file}${COLORS.reset}`));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
console.log();
|
|
342
|
+
log(` ${COLORS.green}✓${COLORS.reset} ${COLORS.bold}${COLORS.green}Installation complete!${COLORS.reset}`, 'green');
|
|
343
|
+
console.log();
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
main().catch(err => {
|
|
347
|
+
log(`\n ${COLORS.red}✗${COLORS.reset} Fatal error: ${err.message}\n`, 'red');
|
|
348
|
+
process.exit(1);
|
|
349
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ijuantm/simpl-addon",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool to install Simpl framework add-ons.",
|
|
5
|
+
"main": "install.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"simpl-addon": "install.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=20.0.0"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/IJuanTM/simpl"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"simpl",
|
|
18
|
+
"addon",
|
|
19
|
+
"installer",
|
|
20
|
+
"cli",
|
|
21
|
+
"php-framework"
|
|
22
|
+
],
|
|
23
|
+
"author": "Iwan van der Wal",
|
|
24
|
+
"license": "GPL-3.0-only",
|
|
25
|
+
"homepage": "https://simpl.iwanvanderwal.nl/"
|
|
26
|
+
}
|