@ijuantm/simpl-addon 2.6.5 → 2.7.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 +228 -251
  2. package/package.json +1 -1
package/install.js CHANGED
@@ -9,33 +9,52 @@ const {exec} = require('child_process');
9
9
 
10
10
  const execAsync = promisify(exec);
11
11
 
12
- const COLORS = {
12
+ const C = {
13
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
+ cyan: '\x1b[36m', blue: '\x1b[34m', gray: '\x1b[90m', bold: '\x1b[1m', dim: '\x1b[2m',
15
15
  };
16
16
 
17
17
  const CDN_BASE = 'https://cdn.simpl.iwanvanderwal.nl/framework';
18
18
  const LOCAL_RELEASES_DIR = process.env.SIMPL_LOCAL_RELEASES || path.join(process.cwd(), 'local-releases');
19
19
  const BOX_WIDTH = 62;
20
+ const PAD = ' ';
20
21
 
21
- const log = (message = '', color = 'reset') => console.log(`${COLORS[color]}${message}${COLORS.reset}`);
22
+ const styled = (msg, ...styles) => styles.join('') + msg + C.reset;
23
+ const line = (msg = '') => console.log(msg);
24
+ const out = (msg, color = C.reset) => console.log(color + msg + C.reset);
25
+ const prefixed = (symbol, color, msg, bold = false, dim = false) => out(PAD + color + symbol + C.reset + ' ' + (bold ? styled(msg, C.bold) : dim ? styled(msg, C.dim) : msg));
26
+
27
+ const success = (msg, bold = false) => prefixed('✓', C.green, msg, bold);
28
+ const error = (msg, bold = false) => prefixed('✕', C.red, msg, bold);
29
+ const warn = (msg, bold = false) => prefixed('⚠', C.yellow, msg, bold);
30
+ const info = (msg) => prefixed('ℹ', C.cyan, msg, false, true);
31
+ const task = (msg) => out(PAD + msg);
32
+ const item = (msg, dim = false) => out(PAD + C.cyan + '•' + C.reset + ' ' + (dim ? styled(msg, C.dim) : msg));
22
33
 
23
34
  const box = (title) => {
24
- log();
25
- log(` ╭${'─'.repeat(BOX_WIDTH)}╮`);
26
- const contentWidth = BOX_WIDTH - 2; // Remove the two padding characters on each side
27
- const remainingSpace = contentWidth - (typeof title === 'string' ? title.length : 0);
28
- const padding = remainingSpace > 0 ? ' '.repeat(remainingSpace) : '';
29
- log(` ${COLORS.bold}${title}${COLORS.reset}${padding}│`);
30
- log(` ╰${''.repeat(BOX_WIDTH)}╯`);
31
- log();
35
+ const plain = title.replace(/\x1b\[[0-9;]*m/g, '');
36
+ const truncated = plain.length > BOX_WIDTH - 2 ? plain.slice(0, BOX_WIDTH - 5) + '...' : plain;
37
+ const displayTitle = title.replace(plain, truncated);
38
+ const spaces = ' '.repeat(BOX_WIDTH - 2 - truncated.length);
39
+ line();
40
+ out(PAD + '╭' + '─'.repeat(BOX_WIDTH) + '╮');
41
+ out(PAD + '' + styled(displayTitle, C.bold) + spaces + ' │');
42
+ out(PAD + '╰' + '─'.repeat(BOX_WIDTH) + '╯');
43
+ line();
44
+ };
45
+
46
+ const divider = () => {
47
+ line();
48
+ out(PAD + '─'.repeat(16), C.dim);
49
+ line();
32
50
  };
33
51
 
52
+ const printAnswer = (question, value) => out(`${question}: ${C.cyan}${value}${C.reset}`);
53
+
34
54
  const fetchUrl = (url) => new Promise((resolve, reject) => {
35
55
  https.get(url, res => {
36
- if (res.statusCode === 302 || res.statusCode === 301) return fetchUrl(res.headers.location).then(resolve).catch(reject);
56
+ if (res.statusCode === 301 || res.statusCode === 302) return fetchUrl(res.headers.location).then(resolve).catch(reject);
37
57
  if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage || 'Request failed'}`));
38
-
39
58
  let data = '';
40
59
  res.on('data', chunk => data += chunk);
41
60
  res.on('end', () => resolve(data));
@@ -44,52 +63,38 @@ const fetchUrl = (url) => new Promise((resolve, reject) => {
44
63
 
45
64
  const downloadFile = (url, dest) => new Promise((resolve, reject) => {
46
65
  const file = fs.createWriteStream(dest);
66
+ const fail = (err) => {
67
+ try {
68
+ fs.unlinkSync(dest);
69
+ } catch {
70
+ }
71
+ reject(err);
72
+ };
47
73
 
48
74
  https.get(url, res => {
49
- if (res.statusCode === 302 || res.statusCode === 301) {
75
+ if (res.statusCode === 301 || res.statusCode === 302) {
50
76
  fs.unlinkSync(dest);
51
77
  return downloadFile(res.headers.location, dest).then(resolve).catch(reject);
52
78
  }
53
- if (res.statusCode !== 200) {
54
- fs.unlinkSync(dest);
55
- return reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage || 'Request failed'}`));
56
- }
57
-
79
+ if (res.statusCode !== 200) return fail(new Error(`HTTP ${res.statusCode}: ${res.statusMessage || 'Request failed'}`));
58
80
  res.pipe(file);
59
81
  file.on('finish', () => {
60
82
  file.close();
61
83
  resolve();
62
84
  });
63
- }).on('error', err => {
64
- fs.unlinkSync(dest);
65
- reject(err);
66
- });
67
-
68
- file.on('error', err => {
69
- fs.unlinkSync(dest);
70
- reject(err);
71
- });
85
+ }).on('error', fail);
86
+ file.on('error', fail);
72
87
  });
73
88
 
74
89
  const promptUser = (question, defaultValue = '') => new Promise(resolve => {
75
90
  const rl = readline.createInterface({input: process.stdin, output: process.stdout});
76
- const prompt = defaultValue ? `${question} ${COLORS.dim}(${defaultValue})${COLORS.reset}: ` : `${question}: `;
91
+ const prompt = defaultValue ? `${question} ${C.dim}(${defaultValue})${C.reset}: ` : `${question}: `;
77
92
  rl.question(prompt, answer => {
78
93
  rl.close();
79
94
  resolve(answer.trim() || defaultValue);
80
95
  });
81
96
  });
82
97
 
83
- const printAnswer = (question, value) => console.log(`${question}: ${COLORS.cyan}${value}${COLORS.reset}`);
84
-
85
- const getSimplVersion = () => {
86
- const simplFile = path.join(process.cwd(), '.simpl');
87
- if (!fs.existsSync(simplFile)) throw new Error('Not a Simpl project. Missing .simpl file in current directory.');
88
- const config = JSON.parse(fs.readFileSync(simplFile, 'utf8'));
89
- if (!config.version) throw new Error('Invalid .simpl file: missing version field');
90
- return config.version;
91
- };
92
-
93
98
  const parseArgs = (args) => {
94
99
  const result = {addon: null, unknownFlags: [], help: false, list: false};
95
100
  for (const arg of args) {
@@ -124,77 +129,71 @@ const closestMatch = (input, options) => {
124
129
  return bestDist <= Math.max(3, Math.floor(input.length / 2)) ? best : null;
125
130
  };
126
131
 
132
+ const listAddons = (addons) => addons.forEach((name, i) => out(PAD + C.cyan + `${i + 1}.` + C.reset + ' ' + name));
133
+
127
134
  const promptAddon = async (addons, firstInput = null) => {
128
135
  const askSuggestion = async (input) => {
129
136
  const suggestion = closestMatch(input, addons);
130
- log();
131
- log(` ${COLORS.red}✗${COLORS.reset} Add-on ${COLORS.bold}${input}${COLORS.reset} not found`);
132
-
137
+ line();
138
+ error(`Add-on ${styled(input, C.bold)} not found`);
133
139
  if (suggestion) {
134
- log(` Did you mean: ${COLORS.blue}${suggestion}${COLORS.reset}?`);
135
- log();
136
-
140
+ out(PAD + `Did you mean: ${C.blue}${suggestion}${C.reset}?`);
141
+ line();
137
142
  while (true) {
138
- const answer = await promptUser(` Use "${suggestion}"? ${COLORS.dim}(yes / no (lists available add-ons))${COLORS.reset}`);
139
- const a = answer.toLowerCase();
143
+ const a = (await promptUser(PAD + `Use "${suggestion}"? ${C.dim}(yes / no (lists available add-ons))${C.reset}`)).toLowerCase();
140
144
  if (a === 'yes' || a === 'y') return suggestion;
141
145
  if (a === 'no' || a === 'n') break;
142
- log(` ${COLORS.yellow}⚠${COLORS.reset} ${COLORS.dim}Please answer yes or no${COLORS.reset}`);
146
+ warn('Please answer yes or no');
143
147
  }
144
148
  }
145
-
146
- log();
147
- log(` ${COLORS.bold}Available add-ons:${COLORS.reset}`, 'blue');
149
+ line();
150
+ out(PAD + styled('Available add-ons:', C.bold), C.blue);
148
151
  listAddons(addons);
149
- log();
152
+ line();
150
153
  return null;
151
154
  };
152
155
 
153
156
  let pending = firstInput;
154
157
  while (true) {
155
- const input = pending || await promptUser(` Add-on to install ${COLORS.dim}(name or number)${COLORS.reset}`);
158
+ const input = pending || await promptUser(PAD + `Add-on to install ${C.dim}(name or number)${C.reset}`);
156
159
  pending = null;
157
-
158
160
  if (!input) {
159
- log(` ${COLORS.yellow}⚠${COLORS.reset} ${COLORS.dim}Selection cannot be empty${COLORS.reset}`);
160
- log();
161
+ warn('Selection cannot be empty');
162
+ line();
161
163
  continue;
162
164
  }
163
-
164
- const numInput = parseInt(input, 10);
165
- if (!isNaN(numInput) && numInput >= 1 && numInput <= addons.length) return addons[numInput - 1];
165
+ const num = parseInt(input, 10);
166
+ if (!isNaN(num) && num >= 1 && num <= addons.length) return addons[num - 1];
166
167
  if (addons.includes(input)) return input;
167
-
168
168
  const resolved = await askSuggestion(input);
169
169
  if (resolved) return resolved;
170
170
  }
171
171
  };
172
172
 
173
- const listAddons = (addons) => addons.forEach((name, i) => log(` ${COLORS.cyan}${i + 1}.${COLORS.reset} ${name}`));
174
-
175
173
  const showHelp = () => {
176
174
  box('Simpl Add-on Installer');
177
- log(` ${COLORS.bold}Usage:${COLORS.reset}`, 'blue');
178
- log(` ${COLORS.dim}npx @ijuantm/simpl-addon${COLORS.reset}`);
179
- log(` ${COLORS.dim}npx @ijuantm/simpl-addon --addon=<name>${COLORS.reset}`);
180
- log(` ${COLORS.dim}npx @ijuantm/simpl-addon --help${COLORS.reset}`);
181
- log();
182
- log(` ${COLORS.bold}Options:${COLORS.reset}`, 'blue');
183
- log(` ${COLORS.dim}--addon=<name>, -a=<name>${COLORS.reset} Add-on to install`);
184
- log(` ${COLORS.dim}--list, -l${COLORS.reset} List available add-ons`);
185
- log(` ${COLORS.dim}--help, -h${COLORS.reset} Show this help message`);
186
- log();
187
- log(` ${COLORS.bold}Note:${COLORS.reset}`, 'blue');
188
- log(` Run this command from the root of your Simpl project.`);
189
- log(` The add-on version will match your Simpl framework version.`);
190
- log();
175
+ out(PAD + styled('Usage:', C.bold), C.blue);
176
+ out(PAD + styled('npx @ijuantm/simpl-addon', C.dim));
177
+ out(PAD + styled('npx @ijuantm/simpl-addon --addon=<name>', C.dim));
178
+ out(PAD + styled('npx @ijuantm/simpl-addon --help', C.dim));
179
+ line();
180
+ out(PAD + styled('Options:', C.bold), C.blue);
181
+ out(PAD + styled('--addon=<name>, -a=<name>', C.dim) + ' Add-on to install');
182
+ out(PAD + styled('--list, -l', C.dim) + ' List available add-ons');
183
+ out(PAD + styled('--help, -h', C.dim) + ' Show this help message');
184
+ line();
185
+ out(PAD + styled('Note:', C.bold), C.blue);
186
+ item('Run this command from the root of your Simpl project.');
187
+ item('The add-on version will match your Simpl framework version.');
188
+ line();
191
189
  };
192
190
 
193
191
  const checkServerAvailability = () => new Promise(resolve => {
194
192
  https.get(`${CDN_BASE}/versions.json`, {timeout: 5000}, res => {
195
193
  res.resume();
196
194
  resolve(res.statusCode === 200);
197
- }).on('error', () => resolve(false)).on('timeout', () => resolve(false));
195
+ })
196
+ .on('error', () => resolve(false)).on('timeout', () => resolve(false));
198
197
  });
199
198
 
200
199
  const getVersionsData = async () => {
@@ -202,28 +201,32 @@ const getVersionsData = async () => {
202
201
  return JSON.parse(await fetchUrl(`${CDN_BASE}/versions.json`));
203
202
  };
204
203
 
204
+ const getSimplVersion = () => {
205
+ const simplFile = path.join(process.cwd(), '.simpl');
206
+ if (!fs.existsSync(simplFile)) throw new Error('Not a Simpl project. Missing .simpl file in current directory.');
207
+ const config = JSON.parse(fs.readFileSync(simplFile, 'utf8'));
208
+ if (!config.version) throw new Error('Invalid .simpl file: missing version field');
209
+ return config.version;
210
+ };
211
+
205
212
  const getAvailableAddons = async (version) => {
206
213
  const localAddonsDir = path.join(LOCAL_RELEASES_DIR, version, 'add-ons');
207
-
208
214
  if (fs.existsSync(localAddonsDir)) return fs.readdirSync(localAddonsDir, {withFileTypes: true})
209
- .filter(entry => entry.isFile() && entry.name.endsWith('.zip'))
210
- .map(entry => entry.name.replace('.zip', ''))
215
+ .filter(e => e.isFile() && e.name.endsWith('.zip'))
216
+ .map(e => e.name.slice(0, -4))
211
217
  .sort();
212
-
213
- const versionMeta = (await getVersionsData()).versions[version];
214
- return (versionMeta?.['add-ons'] || []).sort();
218
+ return ((await getVersionsData()).versions[version]?.['add-ons'] || []).sort();
215
219
  };
216
220
 
217
221
  const extractMarkers = (content) => {
218
222
  const markers = [];
219
223
  content.split('\n').forEach((line, i) => {
220
- const afterMatch = line.match(/@addon-insert:after\s*\(\s*(["'])(.*?)\1\s*\)/);
221
- const beforeMatch = line.match(/@addon-insert:before\s*\(\s*(["'])(.*?)\1\s*\)/);
222
- const replaceMatch = line.match(/@addon-insert:replace\s*\(\s*(["'])(.*?)\1\s*\)/);
223
-
224
- if (afterMatch) markers.push({type: 'after', lineIndex: i, searchText: afterMatch[2]});
225
- else if (beforeMatch) markers.push({type: 'before', lineIndex: i, searchText: beforeMatch[2]});
226
- else if (replaceMatch) markers.push({type: 'replace', lineIndex: i, markerName: replaceMatch[2]});
224
+ const after = line.match(/@addon-insert:after\s*\(\s*(["'])(.*?)\1\s*\)/);
225
+ const before = line.match(/@addon-insert:before\s*\(\s*(["'])(.*?)\1\s*\)/);
226
+ const replace = line.match(/@addon-insert:replace\s*\(\s*(["'])(.*?)\1\s*\)/);
227
+ if (after) markers.push({type: 'after', lineIndex: i, searchText: after[2]});
228
+ else if (before) markers.push({type: 'before', lineIndex: i, searchText: before[2]});
229
+ else if (replace) markers.push({type: 'replace', lineIndex: i, markerName: replace[2]});
227
230
  else if (line.includes('@addon-insert:prepend')) markers.push({type: 'prepend', lineIndex: i});
228
231
  else if (line.includes('@addon-insert:append')) markers.push({type: 'append', lineIndex: i});
229
232
  });
@@ -239,20 +242,21 @@ const collectContentBetweenMarkers = (lines, startIndex) => {
239
242
  return content;
240
243
  };
241
244
 
242
- const normalizeContent = (lines) => lines.map(l => l.trim()).filter(l => l && !l.startsWith('//') && !l.startsWith('#') && !l.startsWith('/*') && !l.startsWith('*')).join('|');
245
+ const normalizeContent = (lines) => lines.map(l => l.trim())
246
+ .filter(l => l && !l.startsWith('//') && !l.startsWith('#') && !l.startsWith('/*') && !l.startsWith('*'))
247
+ .join('|');
243
248
 
244
249
  const processEnvContent = (content, targetContent) => {
245
250
  const envVarsToAdd = [], comments = [];
246
- content.forEach(line => {
251
+ for (const line of content) {
247
252
  const trimmed = line.trim();
248
253
  if (trimmed.startsWith('#') || !trimmed) {
249
254
  comments.push(line);
250
- return;
255
+ continue;
251
256
  }
252
-
253
257
  const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
254
258
  if (match && !new RegExp(`^${match[1]}=`, 'm').test(targetContent)) envVarsToAdd.push(line);
255
- });
259
+ }
256
260
  return {content: [...comments, ...envVarsToAdd], count: envVarsToAdd.length};
257
261
  };
258
262
 
@@ -267,28 +271,24 @@ const mergeFile = (targetPath, addonContent, markers, isEnv = false) => {
267
271
  const operations = [];
268
272
  let newContent = targetContent;
269
273
 
270
- markers.forEach(marker => {
274
+ for (const marker of markers) {
271
275
  let content = collectContentBetweenMarkers(addonLines, marker.lineIndex);
272
- if (content.length === 0) return;
273
-
276
+ if (!content.length) continue;
274
277
  let lineCount = content.length;
275
278
 
276
279
  if (isEnv) {
277
280
  const processed = processEnvContent(content, newContent);
278
281
  content = processed.content;
279
282
  lineCount = processed.count;
280
-
281
- if (content.length === 0) {
283
+ if (!content.length) {
282
284
  operations.push({success: false, type: marker.type, lines: 0, searchText: marker.searchText});
283
- return;
285
+ continue;
284
286
  }
285
287
  } else {
286
288
  const signature = normalizeContent(content);
287
- const targetSignature = normalizeContent(newContent.split('\n'));
288
-
289
- if (signature && targetSignature.includes(signature)) {
289
+ if (signature && normalizeContent(newContent.split('\n')).includes(signature)) {
290
290
  operations.push({success: false, type: marker.type, lines: content.length, searchText: marker.searchText || marker.markerName});
291
- return;
291
+ continue;
292
292
  }
293
293
  }
294
294
 
@@ -302,65 +302,63 @@ const mergeFile = (targetPath, addonContent, markers, isEnv = false) => {
302
302
  } else if (marker.type === 'replace' && marker.markerName) {
303
303
  const targetLines = newContent.split('\n');
304
304
  const replaceIndex = findInsertIndex(targetLines, marker.markerName, 'before');
305
-
306
305
  if (replaceIndex === -1) {
307
306
  operations.push({success: false, type: 'notfound', searchText: marker.markerName});
308
- return;
307
+ continue;
309
308
  }
310
-
311
309
  targetLines.splice(replaceIndex, 1, ...content);
312
310
  newContent = targetLines.join('\n');
313
311
  operations.push({success: true, type: 'replace', lines: lineCount, markerName: marker.markerName});
314
312
  } else if ((marker.type === 'after' || marker.type === 'before') && marker.searchText) {
315
313
  const targetLines = newContent.split('\n');
316
314
  const insertIndex = findInsertIndex(targetLines, marker.searchText, marker.type);
317
-
318
315
  if (insertIndex === -1) {
319
316
  operations.push({success: false, type: 'notfound', searchText: marker.searchText});
320
- return;
317
+ continue;
321
318
  }
322
-
323
319
  targetLines.splice(insertIndex, 0, ...content);
324
320
  newContent = targetLines.join('\n');
325
321
  operations.push({success: true, type: marker.type, lines: lineCount, searchText: marker.searchText});
326
322
  }
327
- });
323
+ }
328
324
 
329
325
  if (newContent !== targetContent) fs.writeFileSync(targetPath, newContent, 'utf8');
330
326
  return {modified: newContent !== targetContent, operations};
331
327
  };
332
328
 
333
329
  const printMergeResults = (relativePath, isEnv, result) => {
334
- const indent = ' ';
335
330
  const varText = isEnv ? 'environment variable' : 'line';
336
331
  let hasChanges = false;
337
332
 
338
- result.operations.forEach(op => {
333
+ for (const op of result.operations) {
339
334
  if (op.success) {
340
335
  hasChanges = true;
341
- 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`);
342
- 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`);
343
- 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' : ''}`);
344
- 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}`);
345
- 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}`);
336
+ const count = `${styled(String(op.lines), C.bold)} ${varText}${op.lines !== 1 ? 's' : ''}`;
337
+ if (op.type === 'prepend') success(`Prepended ${count} to file start`);
338
+ else if (op.type === 'append') success(`Appended ${count} to file end`);
339
+ else if (op.type === 'replace') success(`Replaced marker ${C.cyan}${op.markerName}${C.reset} with ${count}`);
340
+ else if (op.type === 'after') success(`Inserted ${count} ${C.cyan}after${C.reset} ${styled(op.searchText, C.dim)}`);
341
+ else if (op.type === 'before') success(`Inserted ${count} ${C.cyan}before${C.reset} ${styled(op.searchText, C.dim)}`);
346
342
  } else if (op.type === 'notfound') {
347
- const target = op.markerName ? `marker ${COLORS.dim}${op.markerName}${COLORS.reset}` : `${COLORS.dim}${op.searchText}${COLORS.reset}`;
348
- log(`${indent}${COLORS.yellow}⚠${COLORS.reset} ${COLORS.yellow}Could not find target:${COLORS.reset} ${target}`);
349
- } else log(`${indent}${COLORS.gray}○${COLORS.reset} ${COLORS.dim}Content already exists (${op.type})${COLORS.reset}`);
350
- });
343
+ warn(`Could not find target: ${op.markerName ? `marker ${styled(op.markerName, C.dim)}` : styled(op.searchText, C.dim)}`);
344
+ } else {
345
+ info(`Content already exists (${op.type})`);
346
+ }
347
+ }
351
348
 
352
349
  return hasChanges;
353
350
  };
354
351
 
355
352
  const extractZip = async (zipPath, destDir) => {
356
353
  fs.mkdirSync(destDir, {recursive: true});
357
- const cmd = process.platform === 'win32' ? `powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"` : `unzip -q "${zipPath}" -d "${destDir}"`;
354
+ const cmd = process.platform === 'win32'
355
+ ? `powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`
356
+ : `unzip -q "${zipPath}" -d "${destDir}"`;
358
357
  await execAsync(cmd);
359
-
360
358
  const entries = fs.readdirSync(destDir, {withFileTypes: true});
361
359
  if (entries.length === 1 && entries[0].isDirectory()) {
362
360
  const nestedDir = path.join(destDir, entries[0].name);
363
- fs.readdirSync(nestedDir).forEach(item => fs.renameSync(path.join(nestedDir, item), path.join(destDir, item)));
361
+ for (const item of fs.readdirSync(nestedDir)) fs.renameSync(path.join(nestedDir, item), path.join(destDir, item));
364
362
  fs.rmdirSync(nestedDir);
365
363
  }
366
364
  return destDir;
@@ -369,18 +367,19 @@ const extractZip = async (zipPath, destDir) => {
369
367
  const processAddonFiles = (addonDir, targetDir) => {
370
368
  const copied = [], skipped = [], toMerge = [];
371
369
 
372
- const processDirectory = (dir, basePath = '') => fs.readdirSync(dir, {withFileTypes: true}).forEach(entry => {
373
- if (entry.name === 'README.md') return;
370
+ const processDirectory = (dir, basePath = '') => {
371
+ for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
372
+ if (entry.name === 'README.md') continue;
373
+ const srcPath = path.join(dir, entry.name);
374
+ const relativePath = path.join(basePath, entry.name).replace(/\\/g, '/');
375
+ const destPath = path.join(targetDir, relativePath);
374
376
 
375
- const srcPath = path.join(dir, entry.name);
376
- const relativePath = path.join(basePath, entry.name).replace(/\\/g, '/');
377
- const destPath = path.join(targetDir, relativePath);
377
+ if (entry.isDirectory()) {
378
+ processDirectory(srcPath, relativePath);
379
+ continue;
380
+ }
378
381
 
379
- if (entry.isDirectory()) {
380
- processDirectory(srcPath, relativePath);
381
- } else {
382
382
  const content = fs.readFileSync(srcPath, 'utf8');
383
-
384
383
  if (fs.existsSync(destPath)) {
385
384
  const markers = extractMarkers(content);
386
385
  if (markers.length > 0 || entry.name === '.env') toMerge.push({content, destPath, relativePath, markers});
@@ -391,7 +390,7 @@ const processAddonFiles = (addonDir, targetDir) => {
391
390
  copied.push(relativePath);
392
391
  }
393
392
  }
394
- });
393
+ };
395
394
 
396
395
  processDirectory(addonDir);
397
396
  return {copied, skipped, toMerge};
@@ -403,8 +402,8 @@ const downloadAddon = async (addonName, version, targetDir) => {
403
402
 
404
403
  try {
405
404
  if (fs.existsSync(localZipPath)) {
406
- log();
407
- log(` 💻 Using local add-on files`, 'bold');
405
+ line();
406
+ task('💻 Using local add-on files');
408
407
  const sourceDir = await extractZip(localZipPath, tempExtract);
409
408
  const result = processAddonFiles(sourceDir, targetDir);
410
409
  fs.rmSync(tempExtract, {recursive: true, force: true});
@@ -412,7 +411,6 @@ const downloadAddon = async (addonName, version, targetDir) => {
412
411
  }
413
412
 
414
413
  if (!await checkServerAvailability()) throw new Error('CDN server is currently unreachable');
415
-
416
414
  const tempZip = path.join(process.cwd(), `temp-addon-${addonName}.zip`);
417
415
  await downloadFile(`${CDN_BASE}/${version}/add-ons/${addonName}.zip`, tempZip);
418
416
  const sourceDir = await extractZip(tempZip, tempExtract);
@@ -420,213 +418,192 @@ const downloadAddon = async (addonName, version, targetDir) => {
420
418
  fs.unlinkSync(tempZip);
421
419
  fs.rmSync(tempExtract, {recursive: true, force: true});
422
420
  return result;
423
- } catch (error) {
421
+ } catch (err) {
424
422
  if (fs.existsSync(tempExtract)) fs.rmSync(tempExtract, {recursive: true, force: true});
425
- throw error;
423
+ throw err;
426
424
  }
427
425
  };
428
426
 
429
427
  const mergeFiles = (toMerge) => {
430
- if (toMerge.length === 0) return {merged: [], failed: [], unchanged: []};
431
-
428
+ if (!toMerge.length) return {merged: [], failed: [], unchanged: []};
432
429
  const merged = [], failed = [], unchanged = [];
433
430
 
434
- toMerge.forEach(({content, destPath, relativePath, markers}) => {
431
+ for (const {content, destPath, relativePath, markers} of toMerge) {
435
432
  const isEnv = path.basename(destPath) === '.env';
436
- log(`\n ${COLORS.cyan}•${COLORS.reset} ${COLORS.dim}${relativePath}${COLORS.reset}`);
437
-
433
+ line();
434
+ info(styled(relativePath, C.dim));
438
435
  try {
439
- const result = mergeFile(destPath, content, markers, isEnv);
440
- if (printMergeResults(relativePath, isEnv, result)) merged.push(relativePath);
436
+ if (printMergeResults(relativePath, isEnv, mergeFile(destPath, content, markers, isEnv))) merged.push(relativePath);
441
437
  else unchanged.push(relativePath);
442
- } catch (error) {
443
- log(` ${COLORS.red}✗ Error:${COLORS.reset} ${error.message}`);
438
+ } catch (err) {
439
+ error(`Error: ${err.message}`);
444
440
  failed.push(relativePath);
445
441
  }
446
- });
442
+ }
447
443
 
448
444
  return {merged, failed, unchanged};
449
445
  };
450
446
 
451
447
  const main = async () => {
452
- const args = process.argv.slice(2);
453
- const parsed = parseArgs(args);
448
+ const parsed = parseArgs(process.argv.slice(2));
454
449
 
455
450
  if (parsed.help) {
456
451
  showHelp();
457
452
  process.exit(0);
458
453
  }
459
454
 
460
- // Warn about unknown flags and suggest closest known ones
461
455
  for (const flag of parsed.unknownFlags) {
462
456
  const flagName = flag.includes('=') ? flag.slice(0, flag.indexOf('=')) : flag;
463
457
  const suggestion = closestMatch(flagName, KNOWN_FLAGS);
464
- log();
465
- log(` ${COLORS.yellow}⚠${COLORS.reset} Unknown option: ${COLORS.bold}${flag}${COLORS.reset}`, 'yellow');
466
- if (suggestion) log(` ${COLORS.dim}Did you mean ${COLORS.reset}${COLORS.cyan}${suggestion}${COLORS.reset}${COLORS.dim}?${COLORS.reset}`);
458
+ line();
459
+ warn(`Unknown option: ${styled(flag, C.bold)}`);
460
+ if (suggestion) info(`Did you mean ${C.cyan}${suggestion}${C.reset}?`);
467
461
  }
468
-
469
- if (parsed.unknownFlags.length > 0) {
470
- log(` ${COLORS.dim}Run with --help to see all available options.${COLORS.reset}`);
471
- log();
462
+ if (parsed.unknownFlags.length) {
463
+ info('Run with --help to see all available options.');
464
+ line();
472
465
  process.exit(1);
473
466
  }
474
467
 
475
468
  let version;
476
-
477
469
  try {
478
470
  version = getSimplVersion();
479
- } catch (error) {
480
- log();
481
- log(` ${COLORS.red}✗${COLORS.reset} ${error.message}`);
482
- log();
471
+ } catch (err) {
472
+ line();
473
+ error(err.message);
474
+ line();
483
475
  process.exit(1);
484
476
  }
485
477
 
486
- log();
487
- box(`Simpl Add-on Installer${COLORS.dim}(v${version})${COLORS.reset}`);
478
+ line();
479
+ box(`Simpl Add-on Installer ${C.dim}(v${version})${C.reset}`);
488
480
 
489
481
  let versionsData;
490
-
491
482
  try {
492
483
  versionsData = await getVersionsData();
493
- } catch (error) {
494
- log();
495
- log(` ${COLORS.red}✗${COLORS.reset} Failed to fetch version data`);
496
- if (error.message === 'CDN server is currently unreachable') log(` ${COLORS.dim}The CDN server is currently unavailable. Please try again later.${COLORS.reset}`);
497
- log();
484
+ } catch {
485
+ error('Failed to fetch version data');
486
+ info('The CDN server is currently unavailable. Please try again later.');
487
+ line();
498
488
  process.exit(1);
499
489
  }
500
490
 
501
491
  const versionMeta = versionsData.versions[version];
502
492
  if (!versionMeta) {
503
- log();
504
- log(` ${COLORS.red}✗${COLORS.reset} Version ${COLORS.bold}${version}${COLORS.reset} not found`);
505
- log();
493
+ line();
494
+ error(`Version ${styled(version, C.bold)} not found`);
495
+ line();
506
496
  process.exit(1);
507
497
  }
508
498
 
509
499
  if (versionMeta['script-compatible'] === false) {
510
- log();
511
- log(` ${COLORS.red}✗${COLORS.reset} Version ${COLORS.bold}${version}${COLORS.reset} is not compatible with this installer`);
512
- log();
513
- log(` ${COLORS.bold}Manual download:${COLORS.reset}`, 'blue');
514
- log(` ${COLORS.cyan}${CDN_BASE}/${version}/add-ons/`, 'cyan');
515
- log();
516
- log(` ${COLORS.bold}Available add-ons for this version:${COLORS.reset}`, 'blue');
517
-
500
+ line();
501
+ error(`Version ${styled(version, C.bold)} is not compatible with this installer`);
502
+ line();
503
+ out(PAD + styled('Manual download:', C.bold), C.blue);
504
+ item(`${C.cyan}${CDN_BASE}/${version}/add-ons/`);
505
+ line();
506
+ out(PAD + styled('Available add-ons for this version:', C.bold), C.blue);
518
507
  const addons = versionMeta['add-ons'] || [];
519
- if (addons.length === 0) log(` ${COLORS.dim}No add-ons available${COLORS.reset}`);
520
- else addons.forEach(name => {
521
- log(` ${COLORS.cyan}•${COLORS.reset} ${name}: ${COLORS.dim}${CDN_BASE}/${version}/add-ons/${name}.zip${COLORS.reset}`);
522
- });
523
-
524
- log();
508
+ if (!addons.length) info('No add-ons available');
509
+ else for (const name of addons) item(`${name}: ${styled(`${CDN_BASE}/${version}/add-ons/${name}.zip`, C.dim)}`);
510
+ line();
525
511
  process.exit(1);
526
512
  }
527
513
 
528
514
  if (!parsed.addon) {
529
- log();
530
- log(' 🗄️ Fetching available add-ons...', 'bold');
515
+ line();
516
+ task('🗄️ Fetching available add-ons...');
531
517
  }
532
518
 
533
519
  let addons;
534
-
535
520
  try {
536
521
  addons = await getAvailableAddons(version);
537
- } catch (error) {
538
- log();
539
- log(` ${COLORS.red}✗${COLORS.reset} Failed to fetch add-ons`);
540
- if (error.message === 'CDN server is currently unreachable') log(` ${COLORS.dim}The CDN server is currently unavailable. Please try again later.${COLORS.reset}`);
541
- log();
522
+ } catch {
523
+ line();
524
+ error('Failed to fetch add-ons');
525
+ info('The CDN server is currently unavailable. Please try again later.');
526
+ line();
542
527
  process.exit(1);
543
528
  }
544
529
 
545
- if (addons.length === 0) {
546
- log();
547
- log(` ${COLORS.yellow}⚠${COLORS.reset} No add-ons available for this version`);
548
- log();
530
+ if (!addons.length) {
531
+ line();
532
+ warn('No add-ons available for this version');
533
+ line();
549
534
  process.exit(0);
550
535
  }
551
536
 
552
537
  if (parsed.list) {
553
- log();
554
- log(` ${COLORS.bold}Available add-ons:${COLORS.reset}`, 'blue');
538
+ line();
539
+ out(PAD + styled('Available add-ons:', C.bold), C.blue);
555
540
  listAddons(addons);
556
- log();
541
+ line();
557
542
  process.exit(0);
558
543
  }
559
544
 
560
545
  let addonName;
561
-
562
546
  if (parsed.addon) {
563
547
  addonName = await promptAddon(addons, parsed.addon);
564
- printAnswer(' Add-on to install', addonName);
548
+ printAnswer(PAD + 'Add-on to install', addonName);
565
549
  } else {
566
- log();
567
- log(` ${COLORS.bold}Available add-ons:${COLORS.reset}`, 'blue');
550
+ line();
551
+ out(PAD + styled('Available add-ons:', C.bold), C.blue);
568
552
  listAddons(addons);
569
- log();
570
-
553
+ line();
571
554
  addonName = await promptAddon(addons);
572
555
  }
573
556
 
574
- log();
575
- box(`Installing: ${COLORS.cyan}${addonName}${COLORS.reset} ${COLORS.dim}(v${version})${COLORS.reset}`);
576
- log(` 📦 Downloading ${COLORS.cyan}${addonName}${COLORS.reset} add-on...`, 'bold');
557
+ line();
558
+ box(`Installing: ${C.cyan}${addonName}${C.reset} ${C.dim}(v${version})${C.reset}`);
559
+ task(`📦 Downloading ${C.cyan}${addonName}${C.reset} add-on...`);
577
560
 
578
561
  let copied, skipped, toMerge;
579
-
580
562
  try {
581
563
  ({copied, skipped, toMerge} = await downloadAddon(addonName, version, process.cwd()));
582
- } catch (error) {
583
- log();
584
- log(` ${COLORS.red}✗${COLORS.reset} Installation failed`);
585
- if (error.message === 'CDN server is currently unreachable') log(` ${COLORS.dim}The CDN server is currently unavailable. Please try again later.${COLORS.reset}`);
586
- else log(` ${COLORS.dim}Please verify the add-on exists and try again${COLORS.reset}`);
587
- log();
564
+ } catch (err) {
565
+ line();
566
+ error('Installation failed');
567
+ if (err.message === 'CDN server is currently unreachable') info('The CDN server is currently unavailable. Please try again later.');
568
+ else info('Please verify the add-on exists and try again');
569
+ line();
588
570
  process.exit(1);
589
571
  }
590
572
 
591
- if (copied.length > 0) {
592
- log();
593
- log(` ${COLORS.green}✓${COLORS.reset} Copied ${COLORS.bold}${copied.length}${COLORS.reset} new file${copied.length !== 1 ? 's' : ''}`);
573
+ if (copied.length) {
574
+ line();
575
+ success(`Copied ${styled(String(copied.length), C.bold)} new file${copied.length !== 1 ? 's' : ''}`);
594
576
  }
595
577
 
596
- if (skipped.length > 0) {
597
- log();
598
- log(` ${COLORS.gray}○${COLORS.reset} ${COLORS.dim}Skipped ${skipped.length} file${skipped.length !== 1 ? 's' : ''} (no merge markers):${COLORS.reset}`);
599
- skipped.forEach(file => log(` ${COLORS.dim}• ${file}${COLORS.reset}`));
578
+ if (skipped.length) {
579
+ line();
580
+ info(`Skipped ${skipped.length} file${skipped.length !== 1 ? 's' : ''} (no merge markers):`);
581
+ for (const file of skipped) item(file, true);
600
582
  }
601
583
 
602
- if (toMerge.length > 0) {
603
- log();
604
- log(' 🔀 Merging existing files...', 'bold');
584
+ if (toMerge.length) {
585
+ line();
586
+ task('🔀 Merging existing files...');
605
587
  const {merged, failed, unchanged} = mergeFiles(toMerge);
606
-
607
- log();
608
- log(' ' + ''.repeat(16), 'gray');
609
- log();
610
-
611
- if (merged.length > 0) log(` ${COLORS.green}✓${COLORS.reset} Successfully merged ${COLORS.bold}${merged.length}${COLORS.reset} file${merged.length !== 1 ? 's' : ''}`);
612
- if (unchanged.length > 0) log(` ${COLORS.gray}○${COLORS.reset} ${COLORS.dim}${unchanged.length} file${unchanged.length !== 1 ? 's' : ''} unchanged (content already exists)${COLORS.reset}`);
613
-
614
- if (failed.length > 0) {
615
- log();
616
- log(` ${COLORS.yellow}⚠${COLORS.reset} ${COLORS.yellow}${failed.length} file${failed.length !== 1 ? 's' : ''} failed to merge${COLORS.reset}`);
617
- log(` ${COLORS.yellow}Please review manually:${COLORS.reset}`);
618
- failed.forEach(file => log(` ${COLORS.cyan}• ${file}${COLORS.reset}`));
588
+ divider();
589
+ if (merged.length) success(`Successfully merged ${styled(String(merged.length), C.bold)} file${merged.length !== 1 ? 's' : ''}`);
590
+ if (unchanged.length) info(`${unchanged.length} file${unchanged.length !== 1 ? 's' : ''} unchanged (content already exists)`);
591
+ if (failed.length) {
592
+ line();
593
+ warn(`${failed.length} file${failed.length !== 1 ? 's' : ''} failed to merge`);
594
+ warn('Please review manually:');
595
+ for (const file of failed) item(file);
619
596
  }
620
597
  }
621
598
 
622
- log();
623
- log(` ${COLORS.green}✓${COLORS.reset} ${COLORS.bold}${COLORS.green}Installation complete!${COLORS.reset}`, 'green');
624
- log();
599
+ line();
600
+ success(styled('Installation complete!', C.bold, C.green), true);
601
+ line();
625
602
  };
626
603
 
627
604
  main().catch(() => {
628
- log();
629
- log(` ${COLORS.red}✗${COLORS.reset} Fatal error occurred`);
630
- log();
605
+ line();
606
+ error('Fatal error occurred');
607
+ line();
631
608
  process.exit(1);
632
609
  });
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.6.5",
4
+ "version": "2.7.0",
5
5
  "scripts": {
6
6
  "link": "npm link",
7
7
  "unlink": "npm unlink -g @ijuantm/simpl-addon"