@ijuantm/simpl-addon 2.2.0 → 2.4.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.
Files changed (2) hide show
  1. package/install.js +110 -21
  2. package/package.json +1 -1
package/install.js CHANGED
@@ -10,7 +10,8 @@ const {exec} = require('child_process');
10
10
  const execAsync = promisify(exec);
11
11
 
12
12
  const COLORS = {
13
- 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'
13
+ reset: '\x1b[0m', green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m',
14
+ cyan: '\x1b[36m', blue: '\x1b[34m', gray: '\x1b[90m', bold: '\x1b[1m', dim: '\x1b[2m'
14
15
  };
15
16
 
16
17
  const CDN_BASE = 'https://cdn.simpl.iwanvanderwal.nl/framework';
@@ -83,7 +84,7 @@ const getSimplVersion = () => {
83
84
  const showHelp = () => {
84
85
  console.log();
85
86
  log(` ╭${'─'.repeat(62)}╮`);
86
- log(` │ ${COLORS.bold}Simpl Add-on Installer${COLORS.reset}${' '.repeat(38)}│`);
87
+ log(` │ ${COLORS.bold}Simpl Add-on Installer${COLORS.reset}${' '.repeat(36)}│`);
87
88
  log(` ╰${'─'.repeat(62)}╯`);
88
89
  console.log();
89
90
  log(` ${COLORS.bold}Usage:${COLORS.reset}`, 'blue');
@@ -114,14 +115,22 @@ const checkServerAvailability = () => new Promise(resolve => {
114
115
  });
115
116
  });
116
117
 
118
+ const getVersionsData = async () => {
119
+ if (!await checkServerAvailability()) throw new Error('CDN server is currently unreachable');
120
+ return JSON.parse(await fetchUrl(`${CDN_BASE}/versions.json`));
121
+ };
122
+
117
123
  const getAvailableAddons = async (version) => {
118
- const localListPath = path.join(LOCAL_RELEASES_DIR, version, 'add-ons', 'list.json');
124
+ const localAddonsDir = path.join(LOCAL_RELEASES_DIR, version, 'add-ons');
119
125
 
120
- if (fs.existsSync(localListPath)) return JSON.parse(fs.readFileSync(localListPath, 'utf8'))['add-ons'];
126
+ if (fs.existsSync(localAddonsDir)) return fs.readdirSync(localAddonsDir, {withFileTypes: true})
127
+ .filter(entry => entry.isFile() && entry.name.endsWith('.zip'))
128
+ .map(entry => entry.name.replace('.zip', ''))
129
+ .sort();
121
130
 
122
- if (!await checkServerAvailability()) throw new Error('CDN server is currently unreachable');
123
-
124
- return JSON.parse(await fetchUrl(`${CDN_BASE}/${version}/add-ons/list.json`))['add-ons'];
131
+ const versionsData = await getVersionsData();
132
+ const versionMeta = versionsData.versions[version];
133
+ return (versionMeta?.['add-ons'] || []).sort();
125
134
  };
126
135
 
127
136
  const extractMarkers = (content) => {
@@ -130,9 +139,11 @@ const extractMarkers = (content) => {
130
139
  content.split('\n').forEach((line, i) => {
131
140
  const afterMatch = line.match(/@addon-insert:after\s*\(\s*["'](.+?)["']\s*\)/);
132
141
  const beforeMatch = line.match(/@addon-insert:before\s*\(\s*["'](.+?)["']\s*\)/);
142
+ const replaceMatch = line.match(/@addon-insert:replace\s*\(\s*["'](.+?)["']\s*\)/);
133
143
 
134
144
  if (afterMatch) markers.push({type: 'after', lineIndex: i, searchText: afterMatch[1]});
135
145
  else if (beforeMatch) markers.push({type: 'before', lineIndex: i, searchText: beforeMatch[1]});
146
+ else if (replaceMatch) markers.push({type: 'replace', lineIndex: i, markerName: replaceMatch[1]});
136
147
  else if (line.includes('@addon-insert:prepend')) markers.push({type: 'prepend', lineIndex: i});
137
148
  else if (line.includes('@addon-insert:append')) markers.push({type: 'append', lineIndex: i});
138
149
  });
@@ -142,10 +153,13 @@ const extractMarkers = (content) => {
142
153
 
143
154
  const collectContentBetweenMarkers = (lines, startIndex) => {
144
155
  const content = [];
156
+
145
157
  for (let i = startIndex + 1; i < lines.length; i++) {
146
158
  if (lines[i].trim().includes('@addon-end')) break;
159
+
147
160
  content.push(lines[i]);
148
161
  }
162
+
149
163
  return content;
150
164
  };
151
165
 
@@ -174,6 +188,14 @@ const findInsertIndex = (lines, searchText, type) => {
174
188
  return -1;
175
189
  };
176
190
 
191
+ const findMarkerLine = (lines, markerName) => {
192
+ const markerPattern = new RegExp(`@addon-marker\\s*\\(\\s*["']${markerName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}["']\\s*\\)`);
193
+
194
+ for (let i = 0; i < lines.length; i++) if (markerPattern.test(lines[i])) return i;
195
+
196
+ return -1;
197
+ };
198
+
177
199
  const mergeFile = (targetPath, addonContent, markers, isEnv = false) => {
178
200
  const targetContent = fs.readFileSync(targetPath, 'utf8');
179
201
  const addonLines = addonContent.split('\n');
@@ -200,7 +222,7 @@ const mergeFile = (targetPath, addonContent, markers, isEnv = false) => {
200
222
  const targetSignature = normalizeContent(newContent.split('\n'));
201
223
 
202
224
  if (signature && targetSignature.includes(signature)) {
203
- operations.push({success: false, type: marker.type, lines: content.length, searchText: marker.searchText});
225
+ operations.push({success: false, type: marker.type, lines: content.length, searchText: marker.searchText || marker.markerName});
204
226
  return;
205
227
  }
206
228
  }
@@ -212,6 +234,18 @@ const mergeFile = (targetPath, addonContent, markers, isEnv = false) => {
212
234
  if (!newContent.endsWith('\n')) newContent += '\n';
213
235
  newContent += '\n' + content.join('\n') + '\n';
214
236
  operations.push({success: true, type: 'append', lines: lineCount});
237
+ } else if (marker.type === 'replace' && marker.markerName) {
238
+ const targetLines = newContent.split('\n');
239
+ const markerLine = findMarkerLine(targetLines, marker.markerName);
240
+
241
+ if (markerLine === -1) {
242
+ operations.push({success: false, type: 'notfound', markerName: marker.markerName});
243
+ return;
244
+ }
245
+
246
+ targetLines.splice(markerLine, 1, ...content);
247
+ newContent = targetLines.join('\n');
248
+ operations.push({success: true, type: 'replace', lines: lineCount, markerName: marker.markerName});
215
249
  } else if ((marker.type === 'after' || marker.type === 'before') && marker.searchText) {
216
250
  const targetLines = newContent.split('\n');
217
251
  const insertIndex = findInsertIndex(targetLines, marker.searchText, marker.type);
@@ -242,10 +276,13 @@ const printMergeResults = (relativePath, isEnv, result) => {
242
276
  hasChanges = true;
243
277
  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`);
244
278
  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`);
279
+ else if (op.type === 'replace') log(`${indent}${COLORS.green}✓${COLORS.reset} Replaced marker ${COLORS.cyan}${op.markerName}${COLORS.reset} with ${COLORS.bold}${op.lines}${COLORS.reset} ${varText}${op.lines !== 1 ? 's' : ''}`);
245
280
  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}"`);
246
281
  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}"`);
247
- } 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}"`);
248
- else log(`${indent}${COLORS.gray}○${COLORS.reset} ${COLORS.dim}Content already exists (${op.type})${COLORS.reset}`);
282
+ } else if (op.type === 'notfound') {
283
+ const target = op.markerName ? `marker "${COLORS.dim}${op.markerName}${COLORS.reset}"` : `"${COLORS.dim}${op.searchText}${COLORS.reset}"`;
284
+ log(`${indent}${COLORS.yellow}⚠${COLORS.reset} ${COLORS.yellow}Could not find target:${COLORS.reset} ${target}`);
285
+ } else log(`${indent}${COLORS.gray}○${COLORS.reset} ${COLORS.dim}Content already exists (${op.type})${COLORS.reset}`);
249
286
  });
250
287
 
251
288
  return hasChanges;
@@ -373,9 +410,52 @@ const main = async () => {
373
410
 
374
411
  console.log();
375
412
  log(` ╭${'─'.repeat(62)}╮`);
376
- log(` │ ${COLORS.bold}Simpl Add-on Installer${COLORS.reset} ${COLORS.dim}(${version})${COLORS.reset}${' '.repeat(37 - version.length)}│`);
413
+ log(` │ ${COLORS.bold}Simpl Add-on Installer${COLORS.reset} ${COLORS.dim}(v${version})${COLORS.reset}${' '.repeat(36 - version.length)}│`);
377
414
  log(` ╰${'─'.repeat(62)}╯`);
378
415
  console.log();
416
+
417
+ let versionsData;
418
+
419
+ try {
420
+ versionsData = await getVersionsData();
421
+ } catch (error) {
422
+ console.log();
423
+ log(` ${COLORS.red}✗${COLORS.reset} Failed to fetch version data`, 'red');
424
+ if (error.message === 'CDN server is currently unreachable') log(` ${COLORS.dim}The CDN server is currently unavailable. Please try again later.${COLORS.reset}`);
425
+ console.log();
426
+ process.exit(1);
427
+ }
428
+
429
+ const versionMeta = versionsData.versions[version];
430
+ if (!versionMeta) {
431
+ console.log();
432
+ log(` ${COLORS.red}✗${COLORS.reset} Version ${COLORS.bold}${version}${COLORS.reset} not found`, 'red');
433
+ console.log();
434
+ process.exit(1);
435
+ }
436
+
437
+ if (versionMeta['script-compatible'] === false) {
438
+ console.log();
439
+ log(` ${COLORS.red}✗${COLORS.reset} Version ${COLORS.bold}${version}${COLORS.reset} is not compatible with this installer`, 'red');
440
+ console.log();
441
+ log(` ${COLORS.bold}Manual download:${COLORS.reset}`, 'blue');
442
+ log(` ${COLORS.cyan}${CDN_BASE}/${version}/add-ons/`, 'cyan');
443
+ console.log();
444
+ log(` ${COLORS.bold}Available add-ons for this version:${COLORS.reset}`, 'blue');
445
+
446
+ const addons = versionMeta['add-ons'] || [];
447
+ if (addons.length === 0) {
448
+ log(` ${COLORS.dim}No add-ons available${COLORS.reset}`);
449
+ } else {
450
+ addons.forEach(name => {
451
+ log(` ${COLORS.cyan}•${COLORS.reset} ${name}: ${COLORS.dim}${CDN_BASE}/${version}/add-ons/${name}.zip${COLORS.reset}`);
452
+ });
453
+ }
454
+
455
+ console.log();
456
+ process.exit(1);
457
+ }
458
+
379
459
  log(' 📦 Fetching available add-ons...', 'bold');
380
460
 
381
461
  let addons;
@@ -399,29 +479,38 @@ const main = async () => {
399
479
  }
400
480
 
401
481
  log(` ${COLORS.bold}Available add-ons:${COLORS.reset}`, 'blue');
402
- addons.forEach(name => log(` ${COLORS.cyan}•${COLORS.reset} ${name}`));
482
+ addons.forEach((name, index) => log(` ${COLORS.cyan}${index + 1}.${COLORS.reset} ${name}`));
403
483
  console.log();
404
484
 
405
485
  let addonName;
406
486
 
407
487
  while (true) {
408
- addonName = await promptUser(' Add-on to install');
409
- if (!addonName) {
410
- log(` ${COLORS.red}✗${COLORS.reset} Add-on name cannot be empty`, 'red');
488
+ const input = await promptUser(' Add-on to install (name or number)');
489
+
490
+ if (!input) {
491
+ log(` ${COLORS.red}✗${COLORS.reset} Selection cannot be empty`, 'red');
411
492
  console.log();
412
493
  continue;
413
494
  }
414
- if (!addons.includes(addonName)) {
415
- log(` ${COLORS.red}✗${COLORS.reset} Add-on "${addonName}" not found`, 'red');
416
- console.log();
417
- continue;
495
+
496
+ const numInput = parseInt(input, 10);
497
+ if (!isNaN(numInput) && numInput >= 1 && numInput <= addons.length) {
498
+ addonName = addons[numInput - 1];
499
+ break;
500
+ }
501
+
502
+ if (addons.includes(input)) {
503
+ addonName = input;
504
+ break;
418
505
  }
419
- break;
506
+
507
+ log(` ${COLORS.red}✗${COLORS.reset} Invalid selection "${input}"`, 'red');
508
+ console.log();
420
509
  }
421
510
 
422
511
  console.log();
423
512
  log(` ╭${'─'.repeat(62)}╮`);
424
- log(` │ ${COLORS.bold}Installing: ${COLORS.cyan}${addonName}${COLORS.reset} ${COLORS.dim}(${version})${COLORS.reset}${' '.repeat(46 - addonName.length - version.length)}│`);
513
+ log(` │ ${COLORS.bold}Installing: ${COLORS.cyan}${addonName}${COLORS.reset} ${COLORS.dim}(v${version})${COLORS.reset}${' '.repeat(45 - addonName.length - version.length)}│`);
425
514
  log(` ╰${'─'.repeat(62)}╯`);
426
515
  console.log();
427
516
  log(' 📦 Downloading add-on...', 'bold');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ijuantm/simpl-addon",
3
3
  "description": "CLI tool to install Simpl framework add-ons.",
4
- "version": "2.2.0",
4
+ "version": "2.4.0",
5
5
  "scripts": {
6
6
  "link": "npm link",
7
7
  "unlink": "npm unlink -g @ijuantm/simpl-addon"