afront 1.0.24 → 1.0.26

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.
@@ -1 +1 @@
1
- <!DOCTYPE ><html data-build="prod-8cc28e5e1d932d4ecbe78f03c4cee6d9b98733722bda94518d8ecd3dfa2d9d5f"><head><meta http-equiv="Content-Security-Policy" content="base-uri 'self'; object-src 'none'; script-src 'self' 'nonce-wD35/cqQBlhU4s75d79QsQ==' 'nonce-LF88zRZnEpW916DR26RPqg==' 'nonce-gaZO0GLzonvJQTuUruXaDA=='; style-src 'self' https://fonts.googleapis.com 'unsafe-hashes' 'nonce-AgIVyh7SFM2nOmTgQG0GuA==' 'nonce-nBONqt0VRNq1G7xSEmMSSw=='; default-src 'self'; style-src-elem 'self' https://fonts.googleapis.com https://fonts.gstatic.com; font-src 'self' https://fonts.gstatic.com"><meta charset="utf-8"></meta><meta name="viewport" content="width=device-width,initial-scale=1"></meta><title>AFront</title><meta name="title" content="Default title"></meta><meta name="description" content="Default description"></meta><meta name="keywords" content="default, keywords"></meta><link rel="stylesheet" href="style.css" nonce="AgIVyh7SFM2nOmTgQG0GuA=="></link><script src="inline.js" nonce="wD35/cqQBlhU4s75d79QsQ=="></script><script defer="defer" src="/static/js/99-ccf46a26c15904ecb674.js?3728db37c9fc45075c32" nonce="LF88zRZnEpW916DR26RPqg=="></script><script defer="defer" src="/static/js/main-397be3d2ec0a9db18d41.js?3728db37c9fc45075c32" nonce="gaZO0GLzonvJQTuUruXaDA=="></script><link href="/static/css/main-b7c0f6b3ea505263e9a2.css?3728db37c9fc45075c32" rel="stylesheet" nonce="nBONqt0VRNq1G7xSEmMSSw=="></link></head><body><asggenapp id="asggen"></asggenapp></body></html>
1
+ <!DOCTYPE ><html data-build="prod-d7131ef908e5cec4287c48c6fa25bea3755fe01e26b73dfaf23dcf3be6a666d3"><head><meta http-equiv="Content-Security-Policy" content="base-uri 'self'; object-src 'none'; script-src 'self' 'nonce-BnsGeoualWWIicIx97fqcg==' 'nonce-IYKuFUVsYbtRYyXdOdE0+A==' 'nonce-ItfGlHvEjUn+VQbYjNa1gA=='; style-src 'self' https://fonts.googleapis.com 'unsafe-hashes' 'nonce-WEIdtmfNqzF5DeBqHmjp4Q==' 'nonce-OhPZVSUrHyGP9OANcCvJjg=='; default-src 'self'; connect-src 'self' wss:; style-src-elem 'self' https://fonts.googleapis.com https://fonts.gstatic.com; font-src 'self' https://fonts.gstatic.com"><meta charset="utf-8"></meta><meta name="viewport" content="width=device-width,initial-scale=1"></meta><title>AFront</title><meta name="title" content="Default title"></meta><meta name="description" content="Default description"></meta><meta name="keywords" content="default, keywords"></meta><link rel="stylesheet" href="style.css" nonce="WEIdtmfNqzF5DeBqHmjp4Q=="></link><script src="inline.js" nonce="BnsGeoualWWIicIx97fqcg=="></script><script defer="defer" src="/static/js/99-ccf46a26c15904ecb674.js?1b0ea58673dec381b36e" nonce="IYKuFUVsYbtRYyXdOdE0+A=="></script><script defer="defer" src="/static/js/main-397be3d2ec0a9db18d41.js?1b0ea58673dec381b36e" nonce="ItfGlHvEjUn+VQbYjNa1gA=="></script><link href="/static/css/main-b7c0f6b3ea505263e9a2.css?1b0ea58673dec381b36e" rel="stylesheet" nonce="OhPZVSUrHyGP9OANcCvJjg=="></link></head><body><asggenapp id="asggen"></asggenapp></body></html>
@@ -1 +1 @@
1
- <!DOCTYPE ><html data-build="prod-353691ee7880567156136bf5d985c6243f6cdcf19aa2d890cd79b418ff79a74e"><head><meta http-equiv="Content-Security-Policy" content="base-uri 'self'; object-src 'none'; script-src 'self' 'nonce-TFOxuiH9KFVm1wETDHw7Kg==' 'nonce-Guvw/Zxzlz0CwlsUMuIi+A=='; style-src 'self' https://fonts.googleapis.com 'unsafe-hashes' 'nonce-FSz9R2o/bdLTLxb4D6MF0A==' 'nonce-j3EdlPCOcavAvWiDcwA7Kw=='; default-src 'self'; style-src-elem 'self' https://fonts.googleapis.com https://fonts.gstatic.com; font-src 'self' https://fonts.gstatic.com"><meta charset="utf-8"></meta><meta name="viewport" content="width=device-width,initial-scale=1"></meta><title>AFront</title><meta name="title" content="Default title"></meta><meta name="description" content="Default description"></meta><meta name="keywords" content="default, keywords"></meta><link rel="stylesheet" href="style.css" nonce="FSz9R2o/bdLTLxb4D6MF0A=="></link><script src="inline.js" nonce="TFOxuiH9KFVm1wETDHw7Kg=="></script><script defer="defer" src="/static/js/main-02c8c189d5bec6c0a7f1.js?b0771b22447cac787944" nonce="Guvw/Zxzlz0CwlsUMuIi+A=="></script><link href="/static/css/main-b7c0f6b3ea505263e9a2.css?b0771b22447cac787944" rel="stylesheet" nonce="j3EdlPCOcavAvWiDcwA7Kw=="></link></head><body><asggenapp id="asggen"></asggenapp></body></html>
1
+ <!DOCTYPE ><html data-build="prod-cbfb2238e5b50629859764444ed22b13d13b9fd23ea479fd4f3d7b66fcf57be8"><head><meta http-equiv="Content-Security-Policy" content="base-uri 'self'; object-src 'none'; script-src 'self' 'nonce-nsb2A5Fp35VYkJIBhpvomw==' 'nonce-KgXgR+/ZTnIMLzgJDtHVeA=='; style-src 'self' https://fonts.googleapis.com 'unsafe-hashes' 'nonce-B29qVzT91S992Brv6yuQAA==' 'nonce-PmbXyTqSeHrys5xJOJwl9w=='; default-src 'self'; connect-src 'self' wss:; style-src-elem 'self' https://fonts.googleapis.com https://fonts.gstatic.com; font-src 'self' https://fonts.gstatic.com"><meta charset="utf-8"></meta><meta name="viewport" content="width=device-width,initial-scale=1"></meta><title>AFront</title><meta name="title" content="Default title"></meta><meta name="description" content="Default description"></meta><meta name="keywords" content="default, keywords"></meta><link rel="stylesheet" href="style.css" nonce="B29qVzT91S992Brv6yuQAA=="></link><script src="inline.js" nonce="nsb2A5Fp35VYkJIBhpvomw=="></script><script defer="defer" src="/static/js/main-02c8c189d5bec6c0a7f1.js?88025b3d4e5b1872ff26" nonce="KgXgR+/ZTnIMLzgJDtHVeA=="></script><link href="/static/css/main-b7c0f6b3ea505263e9a2.css?88025b3d4e5b1872ff26" rel="stylesheet" nonce="PmbXyTqSeHrys5xJOJwl9w=="></link></head><body><asggenapp id="asggen"></asggenapp></body></html>
package/install.js CHANGED
@@ -1,28 +1,38 @@
1
1
  #!/usr/bin/env node
2
- const fs = require('fs');
3
- const path = require('path');
4
- const { https } = require('follow-redirects');
5
- const { exec } = require('child_process');
6
- const os = require('os');
7
- const tmp = require('tmp');
8
- const AdmZip = require('adm-zip');
9
- const readline = require('readline');
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const https = require("https");
5
+ const { spawn } = require("child_process");
6
+ const os = require("os");
7
+ const AdmZip = require("adm-zip");
8
+ const readline = require("readline");
9
+ const chalk = require("chalk");
10
+
11
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "afront-"));
12
+ const VERSION = "1.0.26";
10
13
 
11
14
  // Configuration
12
- const GITHUB_ZIP_URL = 'https://github.com/Asggen/afront/archive/refs/tags/v1.0.24.zip'; // Updated URL
15
+ const GITHUB_ZIP_URL =
16
+ "https://github.com/Asggen/afront/archive/refs/tags/v1.0.26.zip"; // Updated URL
13
17
 
14
18
  // Define files to skip
15
- const SKIP_FILES = ['FUNDING.yml', 'CODE_OF_CONDUCT.md', 'SECURITY.md', 'install.js', '.npmrc'];
19
+ const SKIP_FILES = [
20
+ "FUNDING.yml",
21
+ "CODE_OF_CONDUCT.md",
22
+ "SECURITY.md",
23
+ "install.js",
24
+ ".npmrc",
25
+ ];
16
26
 
17
27
  // Initialize readline interface
18
28
  const rl = readline.createInterface({
19
29
  input: process.stdin,
20
- output: process.stdout
30
+ output: process.stdout,
21
31
  });
22
32
 
23
33
  // Spinner function
24
34
  const spinner = (text, delay = 100) => {
25
- const spinnerChars = ['|', '/', '-', '\\'];
35
+ const spinnerChars = ["|", "/", "-", "\\"];
26
36
  let i = 0;
27
37
  const interval = setInterval(() => {
28
38
  readline.cursorTo(process.stdout, 0);
@@ -48,44 +58,56 @@ const askQuestion = (question) => {
48
58
  const downloadFile = (url, destination) => {
49
59
  return new Promise((resolve, reject) => {
50
60
  const file = fs.createWriteStream(destination);
51
- const stopSpinner = spinner('Downloading');
52
- https.get(url, (response) => {
53
- if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
54
- return downloadFile(response.headers.location, destination).then(resolve).catch(reject);
61
+ const stopSpinner = spinner(chalk.cyan("⬇ Downloading template..."));
62
+
63
+ const request = https.get(url, (response) => {
64
+ if (
65
+ response.statusCode >= 300 &&
66
+ response.statusCode < 400 &&
67
+ response.headers.location
68
+ ) {
69
+ return downloadFile(response.headers.location, destination)
70
+ .then(resolve)
71
+ .catch(reject);
55
72
  }
73
+
56
74
  if (response.statusCode !== 200) {
57
- reject(new Error(`Failed to download file. Status code: ${response.statusCode}`));
58
- return;
75
+ stopSpinner();
76
+ return reject(
77
+ new Error(
78
+ `Failed to download file. Status code: ${response.statusCode}`,
79
+ ),
80
+ );
59
81
  }
82
+
60
83
  response.pipe(file);
61
- file.on('finish', () => {
84
+
85
+ file.on("finish", () => {
62
86
  file.close(() => {
63
- if (fs.statSync(destination).size > 0) {
64
- stopSpinner();
65
- resolve();
66
- } else {
67
- stopSpinner();
68
- reject(new Error('Downloaded file is empty.'));
69
- }
87
+ stopSpinner();
88
+ resolve();
70
89
  });
71
90
  });
72
- }).on('error', (err) => {
73
- // attempt to remove partial download only if it's inside allowed temp locations
74
- safeUnlink(destination).finally(() => reject(err));
91
+ });
92
+
93
+ request.on("error", (err) => {
94
+ stopSpinner();
95
+ fs.promises.unlink(destination).catch(() => {});
96
+ reject(err);
75
97
  });
76
98
  });
77
99
  };
78
100
 
79
101
  const extractZip = (zipPath, extractTo) => {
80
102
  return new Promise((resolve, reject) => {
81
- const stopSpinner = spinner('Extracting');
103
+ const stopSpinner = spinner(chalk.yellow("📦 Extracting files..."));
82
104
  try {
83
105
  const zip = new AdmZip(zipPath);
84
106
  zip.extractAllTo(extractTo, true);
85
107
  fs.readdir(extractTo, (err, files) => {
86
108
  if (err) {
87
109
  stopSpinner();
88
- console.error('Error reading extracted folder:', err);
110
+ console.error("Error reading extracted folder:", err);
89
111
  reject(err);
90
112
  } else {
91
113
  stopSpinner();
@@ -94,7 +116,7 @@ const extractZip = (zipPath, extractTo) => {
94
116
  });
95
117
  } catch (err) {
96
118
  stopSpinner();
97
- console.error('Error extracting zip file:', err);
119
+ console.error("Error extracting zip file:", err);
98
120
  reject(err);
99
121
  }
100
122
  });
@@ -102,18 +124,44 @@ const extractZip = (zipPath, extractTo) => {
102
124
 
103
125
  const runNpmInstall = (directory) => {
104
126
  return new Promise((resolve, reject) => {
105
- const stopSpinner = spinner('Running npm install');
106
- exec('npm install --legacy-peer-deps --no-audit --no-fund', { cwd: directory }, (err, stdout, stderr) => {
107
- if (err) {
108
- stopSpinner();
109
- console.error('Error running npm install:', stderr);
110
- reject(err);
111
- } else {
112
- stopSpinner();
113
- console.log('npm install output:', stdout);
127
+ const stopSpinner = spinner(chalk.magenta("📦 Installing dependencies (this may take a few seconds)..."));
128
+
129
+ const child = spawn(
130
+ process.platform === "win32" ? "npm.cmd" : "npm",
131
+ [
132
+ "install",
133
+ "--legacy-peer-deps",
134
+ "--no-audit",
135
+ "--no-fund",
136
+ "--loglevel=error",
137
+ ],
138
+ {
139
+ cwd: directory,
140
+ stdio: "pipe",
141
+ shell: false,
142
+ },
143
+ );
144
+
145
+ let errorOutput = "";
146
+
147
+ child.stderr.on("data", (data) => {
148
+ errorOutput += data.toString();
149
+ });
150
+
151
+ child.on("close", (code) => {
152
+ stopSpinner();
153
+ if (code === 0) {
114
154
  resolve();
155
+ } else {
156
+ console.error(chalk.red(errorOutput));
157
+ reject(new Error(`npm install failed with code ${code}`));
115
158
  }
116
159
  });
160
+
161
+ child.on("error", (err) => {
162
+ stopSpinner();
163
+ reject(err);
164
+ });
117
165
  });
118
166
  };
119
167
 
@@ -121,7 +169,7 @@ const createDirIfNotExists = (dirPath) => {
121
169
  return new Promise((resolve, reject) => {
122
170
  fs.mkdir(dirPath, { recursive: true }, (err) => {
123
171
  if (err) {
124
- reject(new Error('Error creating directory:', err));
172
+ reject(new Error("Error creating directory:", err));
125
173
  } else {
126
174
  resolve();
127
175
  }
@@ -135,12 +183,11 @@ const isPathInside = (root, target) => {
135
183
  return t === r || t.startsWith(r + path.sep);
136
184
  };
137
185
 
138
-
139
186
  // Security: path validated against SAFE_UNLINK_ROOT before unlink
140
- const SAFE_UNLINK_ROOT = path.resolve(process.cwd());
187
+ const SAFE_UNLINK_ROOT = fs.realpathSync(process.cwd());
141
188
 
142
189
  const safeUnlink = async (targetPath) => {
143
- const resolved = path.resolve(targetPath);
190
+ const resolved = await fs.promises.realpath(targetPath);
144
191
 
145
192
  if (!resolved.startsWith(SAFE_UNLINK_ROOT + path.sep)) {
146
193
  throw new Error(`Blocked unlink outside safe root: ${resolved}`);
@@ -155,18 +202,24 @@ const safeUnlink = async (targetPath) => {
155
202
  await fs.promises.unlink(resolved);
156
203
  };
157
204
 
158
-
159
205
  const safeRm = (dirPath, cb) => {
160
206
  const resolved = path.resolve(dirPath);
161
207
  if (!isPathInside(process.cwd(), resolved)) {
162
- return cb(new Error(`Refusing to remove directory outside current working directory: ${resolved}`));
208
+ return cb(
209
+ new Error(
210
+ `Refusing to remove directory outside current working directory: ${resolved}`,
211
+ ),
212
+ );
163
213
  }
164
214
  fs.rm(resolved, { recursive: true, force: true }, cb);
165
215
  };
166
216
 
167
217
  // Perform an atomic-safe move: open source with O_NOFOLLOW, stream to a temp file, fsync, then rename
168
218
  const safeMoveFile = async (resolvedSrc, resolvedDest) => {
169
- const srcFlags = fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW;
219
+ const srcFlags =
220
+ process.platform === "win32"
221
+ ? fs.constants.O_RDONLY
222
+ : fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW;
170
223
  let srcHandle;
171
224
  let destHandle;
172
225
  const tmpName = `${resolvedDest}.tmp-${process.pid}-${Date.now()}`;
@@ -180,22 +233,35 @@ const safeMoveFile = async (resolvedSrc, resolvedDest) => {
180
233
  const stats = await srcHandle.stat();
181
234
  if (stats.isDirectory()) {
182
235
  await srcHandle.close();
183
- throw new Error('Source is a directory');
236
+ throw new Error("Source is a directory");
184
237
  }
185
238
 
186
239
  await createDirIfNotExists(path.dirname(resolvedDest));
187
240
 
188
- destHandle = await fs.promises.open(tmpName, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC, stats.mode);
241
+ destHandle = await fs.promises.open(
242
+ tmpName,
243
+ fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC,
244
+ stats.mode,
245
+ );
189
246
 
190
247
  const bufferSize = 64 * 1024;
191
248
  const buffer = Buffer.allocUnsafe(bufferSize);
192
249
  let position = 0;
193
250
  while (true) {
194
- const { bytesRead } = await srcHandle.read(buffer, 0, bufferSize, position);
251
+ const { bytesRead } = await srcHandle.read(
252
+ buffer,
253
+ 0,
254
+ bufferSize,
255
+ position,
256
+ );
195
257
  if (!bytesRead) break;
196
258
  let written = 0;
197
259
  while (written < bytesRead) {
198
- const { bytesWritten } = await destHandle.write(buffer, written, bytesRead - written);
260
+ const { bytesWritten } = await destHandle.write(
261
+ buffer,
262
+ written,
263
+ bytesRead - written,
264
+ );
199
265
  written += bytesWritten;
200
266
  }
201
267
  position += bytesRead;
@@ -211,7 +277,7 @@ const safeMoveFile = async (resolvedSrc, resolvedDest) => {
211
277
  if (destHandle) await destHandle.close();
212
278
  } catch (e) {}
213
279
  try {
214
- await safeUnlink(tmpName, [path.dirname(resolvedDest)]).catch(() => {});
280
+ await fs.promises.unlink(tmpName).catch(() => {});
215
281
  } catch (e) {}
216
282
  try {
217
283
  if (srcHandle) await srcHandle.close();
@@ -221,13 +287,17 @@ const safeMoveFile = async (resolvedSrc, resolvedDest) => {
221
287
  };
222
288
 
223
289
  const promptForFolderName = async () => {
224
- const answer = await askQuestion('AFront: Enter the name of the destination folder: ');
290
+ const answer = await askQuestion(
291
+ "AFront: Enter the name of the destination folder: ",
292
+ );
225
293
  return answer;
226
294
  };
227
295
 
228
296
  const promptForReplace = async (dirPath) => {
229
- const answer = await askQuestion(`The directory ${dirPath} already exists. Do you want to replace it? (yes/no): `);
230
- return answer === 'yes' || answer === 'y';
297
+ const answer = await askQuestion(
298
+ `The directory ${dirPath} already exists. Do you want to replace it? (yes/no): `,
299
+ );
300
+ return answer === "yes" || answer === "y";
231
301
  };
232
302
 
233
303
  const removeDir = (dirPath) => {
@@ -240,101 +310,58 @@ const removeDir = (dirPath) => {
240
310
  });
241
311
  };
242
312
 
243
- const SAFE_READDIR_ROOT = path.resolve(os.tmpdir());
313
+ const safeReaddir = async (dirPath, safeRoot) => {
314
+ const resolvedDir = await fs.promises.realpath(dirPath);
315
+ const resolvedRoot = await fs.promises.realpath(safeRoot);
244
316
 
245
- const safeReaddir = (dirPath, cb) => {
246
- const resolved = path.resolve(dirPath);
247
-
248
- if (!resolved.startsWith(SAFE_READDIR_ROOT + path.sep)) {
249
- return cb(new Error(`Blocked readdir outside safe root: ${resolved}`));
317
+ if (
318
+ !resolvedDir.startsWith(resolvedRoot + path.sep) &&
319
+ resolvedDir !== resolvedRoot
320
+ ) {
321
+ throw new Error(`Blocked readdir outside safe root: ${resolvedDir}`);
250
322
  }
251
323
 
252
- fs.lstat(resolved, (err, stats) => {
253
- if (err) return cb(err);
254
- if (!stats.isDirectory()) {
255
- return cb(new Error(`Not a directory: ${resolved}`));
256
- }
324
+ const stats = await fs.promises.lstat(resolvedDir);
325
+ if (!stats.isDirectory()) {
326
+ throw new Error(`Not a directory: ${resolvedDir}`);
327
+ }
257
328
 
258
- fs.readdir(resolved, cb);
259
- });
329
+ return fs.promises.readdir(resolvedDir);
260
330
  };
261
331
 
332
+ const moveFiles = async (srcPath, destPath, safeRoot) => {
333
+ const files = await safeReaddir(srcPath, safeRoot);
262
334
 
263
- const moveFiles = (srcPath, destPath) => {
264
- return new Promise((resolve, reject) => {
265
- safeReaddir(srcPath, (err, files) => {
266
- if (err) {
267
- return reject(err);
268
- }
269
- let pending = files.length;
270
- if (!pending) return resolve();
271
- files.forEach((file) => {
272
- // Normalize and validate file name to avoid path traversal
273
- const fileName = path.basename(file);
274
- if (SKIP_FILES.includes(fileName)) {
275
- if (!--pending) resolve();
276
- return;
277
- }
335
+ for (const file of files) {
336
+ const fileName = path.basename(file);
337
+ if (SKIP_FILES.includes(fileName)) continue;
278
338
 
279
- const srcRoot = path.resolve(srcPath);
280
- const destRoot = path.resolve(destPath);
281
- const resolvedSrc = path.resolve(srcPath, file);
282
- const resolvedDest = path.resolve(destPath, file);
283
-
284
- // Ensure resolved paths are inside their respective roots
285
- if (
286
- !(resolvedSrc === srcRoot || resolvedSrc.startsWith(srcRoot + path.sep)) ||
287
- !(resolvedDest === destRoot || resolvedDest.startsWith(destRoot + path.sep))
288
- ) {
289
- console.warn(`Skipping suspicious path: ${file}`);
290
- if (!--pending) resolve();
291
- return;
292
- }
339
+ const resolvedSrc = path.resolve(srcPath, file);
340
+ const resolvedDest = path.resolve(destPath, file);
293
341
 
294
- // Use lstat to detect symlinks and refuse to follow them
295
- fs.lstat(resolvedSrc, (err, stats) => {
296
- if (err) {
297
- return reject(err);
298
- }
299
-
300
- if (stats.isSymbolicLink()) {
301
- console.warn(`Skipping symbolic link for safety: ${resolvedSrc}`);
302
- if (!--pending) resolve();
303
- return;
304
- }
305
-
306
- if (stats.isDirectory()) {
307
- createDirIfNotExists(resolvedDest)
308
- .then(() => moveFiles(resolvedSrc, resolvedDest))
309
- .then(() => {
310
- if (!--pending) resolve();
311
- })
312
- .catch(reject);
313
- } else {
314
- // Use atomic-safe move: copy from a non-following FD to temp file, then rename
315
- createDirIfNotExists(path.dirname(resolvedDest))
316
- .then(() => {
317
- safeMoveFile(resolvedSrc, resolvedDest)
318
- .then(() => {
319
- if (!--pending) resolve();
320
- })
321
- .catch(reject);
322
- })
323
- .catch(reject);
324
- }
325
- });
326
- });
327
- });
328
- });
342
+ const stats = await fs.promises.lstat(resolvedSrc);
343
+
344
+ if (stats.isSymbolicLink()) {
345
+ console.warn(`Skipping symbolic link: ${resolvedSrc}`);
346
+ continue;
347
+ }
348
+
349
+ if (stats.isDirectory()) {
350
+ await createDirIfNotExists(resolvedDest);
351
+ await moveFiles(resolvedSrc, resolvedDest, safeRoot);
352
+ } else {
353
+ await createDirIfNotExists(path.dirname(resolvedDest));
354
+ await safeMoveFile(resolvedSrc, resolvedDest);
355
+ }
356
+ }
329
357
  };
330
358
 
331
359
  const main = async () => {
332
360
  try {
333
- const tmpDir = tmp.dirSync({ unsafeCleanup: true });
334
- const zipPath = path.join(tmpDir.name, 'archive.zip');
361
+ const zipPath = path.join(tmpDir, "archive.zip");
335
362
 
336
363
  let folderName = process.argv[2];
337
- if (folderName === '.') {
364
+ if (folderName === ".") {
338
365
  folderName = path.basename(process.cwd());
339
366
  } else if (!folderName) {
340
367
  folderName = await promptForFolderName();
@@ -342,27 +369,35 @@ const main = async () => {
342
369
 
343
370
  // Sanitize the provided folder name to prevent path traversal or absolute paths
344
371
  const sanitizeFolderName = (name) => {
345
- if (!name) return '';
372
+ if (!name) return "";
346
373
  // If user provided '.', use current dir basename
347
- if (name === '.') return path.basename(process.cwd());
374
+ if (name === ".") return path.basename(process.cwd());
348
375
  // Disallow absolute paths
349
376
  if (path.isAbsolute(name)) {
350
- console.warn('Absolute paths are not allowed for destination; using basename.');
377
+ console.warn(
378
+ "Absolute paths are not allowed for destination; using basename.",
379
+ );
351
380
  name = path.basename(name);
352
381
  }
353
382
  // Disallow parent traversal and nested paths
354
- if (name === '..' || name.includes(path.sep) || name.includes('..')) {
355
- console.warn('Path traversal detected in destination name; using basename of input.');
383
+ if (name === ".." || name.includes(path.sep) || name.includes("..")) {
384
+ console.warn(
385
+ "Path traversal detected in destination name; using basename of input.",
386
+ );
356
387
  name = path.basename(name);
357
388
  }
358
389
  // Finally, ensure it's a single path segment
359
390
  name = path.basename(name);
360
- if (!name) throw new Error('Invalid destination folder name');
391
+ if (!name) throw new Error("Invalid destination folder name");
361
392
  return name;
362
393
  };
363
394
 
364
395
  folderName = sanitizeFolderName(folderName);
365
396
 
397
+ console.log(
398
+ chalk.green(`Creating project in ${chalk.bold(`./${folderName}`)}\n`),
399
+ );
400
+
366
401
  const destDir = path.join(process.cwd(), folderName);
367
402
 
368
403
  if (fs.existsSync(destDir)) {
@@ -371,7 +406,7 @@ const main = async () => {
371
406
  await removeDir(destDir);
372
407
  await createDirIfNotExists(destDir);
373
408
  } else {
374
- console.log('Operation aborted.');
409
+ console.log("Operation aborted.");
375
410
  rl.close();
376
411
  process.exit(0);
377
412
  }
@@ -380,36 +415,104 @@ const main = async () => {
380
415
  }
381
416
 
382
417
  await downloadFile(GITHUB_ZIP_URL, zipPath);
383
- console.log('Downloaded successfully.');
384
418
 
385
- await extractZip(zipPath, tmpDir.name);
419
+ await extractZip(zipPath, tmpDir);
386
420
 
387
- const extractedFolderName = fs.readdirSync(tmpDir.name)[0];
388
- const extractedFolderPath = path.join(tmpDir.name, extractedFolderName);
421
+ const extractedItems = fs.readdirSync(tmpDir);
389
422
 
390
- fs.readdirSync(extractedFolderPath);
423
+ const extractedFolderName = extractedItems.find((item) => {
424
+ const fullPath = path.join(tmpDir, item);
425
+ return fs.lstatSync(fullPath).isDirectory();
426
+ });
391
427
 
392
- await moveFiles(extractedFolderPath, destDir);
428
+ if (!extractedFolderName) {
429
+ throw new Error("Extraction failed: no directory found in archive.");
430
+ }
431
+ const extractedFolderPath = path.join(tmpDir, extractedFolderName);
432
+
433
+ await moveFiles(extractedFolderPath, destDir, tmpDir);
393
434
 
394
435
  // Remove any bundled lockfiles from the extracted archive to avoid integrity
395
436
  // checksum mismatches originating from the release zip's lockfile.
396
437
  try {
397
- await safeUnlink(path.join(destDir, 'package-lock.json')).catch(() => {});
398
- await safeUnlink(path.join(destDir, 'npm-shrinkwrap.json')).catch(() => {});
438
+ await safeUnlink(path.join(destDir, "package-lock.json")).catch(() => {});
439
+ await safeUnlink(path.join(destDir, "npm-shrinkwrap.json")).catch(
440
+ () => {},
441
+ );
399
442
  } catch (e) {
400
443
  // ignore
401
444
  }
402
445
 
403
446
  await runNpmInstall(destDir);
404
447
 
448
+ console.log(chalk.green("\n✔ AFront project created successfully!\n"));
449
+
450
+ console.log(chalk.bold("Next steps:"));
451
+ console.log(chalk.cyan(` cd ${folderName}`));
452
+ console.log(chalk.cyan(" npm start\n"));
453
+
454
+ console.log(chalk.gray("Happy building with AFront 🚀"));
455
+
405
456
  rl.close();
406
457
 
458
+ fs.rmSync(tmpDir, { recursive: true, force: true });
407
459
  process.exit(0);
408
460
  } catch (err) {
409
- console.error('Error:', err);
461
+ console.error(chalk.red("✖ Error:"), err.message);
410
462
  rl.close();
411
463
  process.exit(1);
412
464
  }
413
465
  };
414
466
 
467
+ const showBanner = () => {
468
+ console.log(
469
+ chalk.cyan.white(`
470
+ ██████████████████████████████████████████████████
471
+ ██████████████████████████████████████████████████
472
+ ██████████████████████████████████████████████████
473
+ ██████████████████████████████████████████████████
474
+ ██████████████████████████████████████████████████
475
+ ██████████████████████████████████████ ████████
476
+ ████████████████████████████████████ ██████████
477
+ ██████████████████████████████████ ███████████
478
+ ████████████████████████████████ █████████████
479
+ ██████████████████████████████ ███████████████
480
+ ████████████████████████████ █████████████████
481
+ ███████████████████████████ ███████████████████
482
+ █████████████████████████ ████████████████████
483
+ ███████████████████████ ██████████████████████
484
+ █████████████████████ ████████████████████████
485
+ ███████████████████ ██████████████████████████
486
+ █████████████████ ████████████████████████████
487
+ ████████████████ ██████████████████████████████
488
+ ██████████████ ████████████████████████████████
489
+ ████████████ █████████████████████████████████
490
+ ██████████ ███████████████████████████████████
491
+ ████████ █████████████████████████████████████
492
+ ██████████████████████████████████████████████████
493
+ ██████████████████████████████████████████████████
494
+ ██████████████████████████████████████████████████
495
+ ██████████████████████████████████████████████████
496
+ ██████████████████████████████████████████████████
497
+
498
+ `),
499
+ );
500
+
501
+ console.log(
502
+ chalk.cyan.bold(`
503
+ █████╗ ███████╗██████╗ ██████╗ ███╗ ██╗████████╗
504
+ ██╔══██╗██╔════╝██╔══██╗██╔═══██╗████╗ ██║╚══██╔══╝
505
+ ███████║█████╗ ██████╔╝██║ ██║██╔██╗ ██║ ██║
506
+ ██╔══██║██╔══╝ ██╔══██╗██║ ██║██║╚██╗██║ ██║
507
+ ██║ ██║██║ ██║ ██║╚██████╔╝██║ ╚████║ ██║
508
+ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝
509
+ `),
510
+ );
511
+
512
+ console.log(chalk.cyan.bold(`⚡ AFront v${VERSION}\n`));
513
+ console.log(chalk.gray("The Future-Ready Frontend Framework\n"));
514
+ console.log(chalk.gray("🚀 https://afront.dev\n"));
515
+ };
516
+
517
+ showBanner();
415
518
  main();