afront 1.0.25 → 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.
Files changed (45) hide show
  1. package/.babelrc +13 -13
  2. package/.env +1 -1
  3. package/LICENSE +21 -21
  4. package/README.md +94 -94
  5. package/build-prod/manifest.json +25 -25
  6. package/build-prod/offline.html +1023 -1023
  7. package/build-prod/robots.txt +3 -3
  8. package/build-prod-static/manifest.json +25 -25
  9. package/build-prod-static/offline.html +1023 -1023
  10. package/build-prod-static/robots.txt +3 -3
  11. package/install.js +518 -415
  12. package/package.json +102 -102
  13. package/server.js +40 -40
  14. package/src/ARoutes/AFRoutes.js +28 -28
  15. package/src/Api/api.config.js +266 -266
  16. package/src/Api/login.service.js +44 -44
  17. package/src/App.js +28 -28
  18. package/src/Components/Background/MeshGradient.js +18 -18
  19. package/src/Components/Footer/Footer.js +108 -108
  20. package/src/Components/Header/Header.js +149 -149
  21. package/src/Components/Loading/LoadingIndicator.js +12 -12
  22. package/src/Components/Loading/LoadingIndicator.module.css +34 -34
  23. package/src/Components/Loading/LoadingSpinner.js +27 -27
  24. package/src/Components/Loading/LoadingSpinner.module.css +100 -100
  25. package/src/Components/RequireAuth.js +29 -29
  26. package/src/LoadingFallback.js +13 -13
  27. package/src/PageNotFound.js +19 -19
  28. package/src/Pages/Home.js +50 -50
  29. package/src/Pages/Signup.js +230 -230
  30. package/src/Pages/Support.js +68 -68
  31. package/src/Routes/ARoutes.js +66 -66
  32. package/src/Routes/ARoutesStatic.js +83 -83
  33. package/src/Static/appStatic.js +16 -16
  34. package/src/Static/indexStatic.js +13 -13
  35. package/src/Style/App.module.css +11 -11
  36. package/src/Style/MeshGradient.module.css +130 -130
  37. package/src/Style/PageNotFound.module.css +37 -37
  38. package/src/Style/Style.module.css +686 -686
  39. package/src/Style/Support.module.css +185 -185
  40. package/src/Utils/LoadingContext.js +5 -5
  41. package/src/index.js +25 -25
  42. package/webpack.build-prod.js +141 -141
  43. package/webpack.dev.js +127 -127
  44. package/webpack.prod.js +148 -148
  45. package/webpack.ssr.prod.js +97 -97
package/install.js CHANGED
@@ -1,415 +1,518 @@
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');
10
-
11
- // Configuration
12
- const GITHUB_ZIP_URL = 'https://github.com/Asggen/afront/archive/refs/tags/v1.0.25.zip'; // Updated URL
13
-
14
- // Define files to skip
15
- const SKIP_FILES = ['FUNDING.yml', 'CODE_OF_CONDUCT.md', 'SECURITY.md', 'install.js', '.npmrc'];
16
-
17
- // Initialize readline interface
18
- const rl = readline.createInterface({
19
- input: process.stdin,
20
- output: process.stdout
21
- });
22
-
23
- // Spinner function
24
- const spinner = (text, delay = 100) => {
25
- const spinnerChars = ['|', '/', '-', '\\'];
26
- let i = 0;
27
- const interval = setInterval(() => {
28
- readline.cursorTo(process.stdout, 0);
29
- process.stdout.write(`${text} ${spinnerChars[i++]}`);
30
- i = i % spinnerChars.length;
31
- }, delay);
32
-
33
- return () => {
34
- clearInterval(interval);
35
- readline.cursorTo(process.stdout, 0);
36
- process.stdout.write(`${text} Done.\n`);
37
- };
38
- };
39
-
40
- const askQuestion = (question) => {
41
- return new Promise((resolve) => {
42
- rl.question(question, (answer) => {
43
- resolve(answer.trim().toLowerCase());
44
- });
45
- });
46
- };
47
-
48
- const downloadFile = (url, destination) => {
49
- return new Promise((resolve, reject) => {
50
- 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);
55
- }
56
- if (response.statusCode !== 200) {
57
- reject(new Error(`Failed to download file. Status code: ${response.statusCode}`));
58
- return;
59
- }
60
- response.pipe(file);
61
- file.on('finish', () => {
62
- 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
- }
70
- });
71
- });
72
- }).on('error', (err) => {
73
- // attempt to remove partial download only if it's inside allowed temp locations
74
- safeUnlink(destination).finally(() => reject(err));
75
- });
76
- });
77
- };
78
-
79
- const extractZip = (zipPath, extractTo) => {
80
- return new Promise((resolve, reject) => {
81
- const stopSpinner = spinner('Extracting');
82
- try {
83
- const zip = new AdmZip(zipPath);
84
- zip.extractAllTo(extractTo, true);
85
- fs.readdir(extractTo, (err, files) => {
86
- if (err) {
87
- stopSpinner();
88
- console.error('Error reading extracted folder:', err);
89
- reject(err);
90
- } else {
91
- stopSpinner();
92
- resolve();
93
- }
94
- });
95
- } catch (err) {
96
- stopSpinner();
97
- console.error('Error extracting zip file:', err);
98
- reject(err);
99
- }
100
- });
101
- };
102
-
103
- const runNpmInstall = (directory) => {
104
- 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);
114
- resolve();
115
- }
116
- });
117
- });
118
- };
119
-
120
- const createDirIfNotExists = (dirPath) => {
121
- return new Promise((resolve, reject) => {
122
- fs.mkdir(dirPath, { recursive: true }, (err) => {
123
- if (err) {
124
- reject(new Error('Error creating directory:', err));
125
- } else {
126
- resolve();
127
- }
128
- });
129
- });
130
- };
131
-
132
- const isPathInside = (root, target) => {
133
- const r = path.resolve(root);
134
- const t = path.resolve(target);
135
- return t === r || t.startsWith(r + path.sep);
136
- };
137
-
138
-
139
- // Security: path validated against SAFE_UNLINK_ROOT before unlink
140
- const SAFE_UNLINK_ROOT = path.resolve(process.cwd());
141
-
142
- const safeUnlink = async (targetPath) => {
143
- const resolved = path.resolve(targetPath);
144
-
145
- if (!resolved.startsWith(SAFE_UNLINK_ROOT + path.sep)) {
146
- throw new Error(`Blocked unlink outside safe root: ${resolved}`);
147
- }
148
-
149
- const stats = await fs.promises.lstat(resolved);
150
-
151
- if (!stats.isFile()) {
152
- throw new Error(`Refusing to unlink non-file: ${resolved}`);
153
- }
154
-
155
- await fs.promises.unlink(resolved);
156
- };
157
-
158
-
159
- const safeRm = (dirPath, cb) => {
160
- const resolved = path.resolve(dirPath);
161
- if (!isPathInside(process.cwd(), resolved)) {
162
- return cb(new Error(`Refusing to remove directory outside current working directory: ${resolved}`));
163
- }
164
- fs.rm(resolved, { recursive: true, force: true }, cb);
165
- };
166
-
167
- // Perform an atomic-safe move: open source with O_NOFOLLOW, stream to a temp file, fsync, then rename
168
- const safeMoveFile = async (resolvedSrc, resolvedDest) => {
169
- const srcFlags = fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW;
170
- let srcHandle;
171
- let destHandle;
172
- const tmpName = `${resolvedDest}.tmp-${process.pid}-${Date.now()}`;
173
- try {
174
- srcHandle = await fs.promises.open(resolvedSrc, srcFlags);
175
- } catch (err) {
176
- throw err;
177
- }
178
-
179
- try {
180
- const stats = await srcHandle.stat();
181
- if (stats.isDirectory()) {
182
- await srcHandle.close();
183
- throw new Error('Source is a directory');
184
- }
185
-
186
- await createDirIfNotExists(path.dirname(resolvedDest));
187
-
188
- destHandle = await fs.promises.open(tmpName, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC, stats.mode);
189
-
190
- const bufferSize = 64 * 1024;
191
- const buffer = Buffer.allocUnsafe(bufferSize);
192
- let position = 0;
193
- while (true) {
194
- const { bytesRead } = await srcHandle.read(buffer, 0, bufferSize, position);
195
- if (!bytesRead) break;
196
- let written = 0;
197
- while (written < bytesRead) {
198
- const { bytesWritten } = await destHandle.write(buffer, written, bytesRead - written);
199
- written += bytesWritten;
200
- }
201
- position += bytesRead;
202
- }
203
-
204
- await destHandle.sync();
205
- await destHandle.close();
206
- await srcHandle.close();
207
-
208
- await fs.promises.rename(tmpName, resolvedDest);
209
- } catch (err) {
210
- try {
211
- if (destHandle) await destHandle.close();
212
- } catch (e) {}
213
- try {
214
- await safeUnlink(tmpName, [path.dirname(resolvedDest)]).catch(() => {});
215
- } catch (e) {}
216
- try {
217
- if (srcHandle) await srcHandle.close();
218
- } catch (e) {}
219
- throw err;
220
- }
221
- };
222
-
223
- const promptForFolderName = async () => {
224
- const answer = await askQuestion('AFront: Enter the name of the destination folder: ');
225
- return answer;
226
- };
227
-
228
- 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';
231
- };
232
-
233
- const removeDir = (dirPath) => {
234
- return new Promise((resolve, reject) => {
235
- console.log(`Removing existing directory: ${dirPath}`);
236
- safeRm(dirPath, (err) => {
237
- if (err) return reject(err);
238
- resolve();
239
- });
240
- });
241
- };
242
-
243
- const SAFE_READDIR_ROOT = path.resolve(os.tmpdir());
244
-
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}`));
250
- }
251
-
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
- }
257
-
258
- fs.readdir(resolved, cb);
259
- });
260
- };
261
-
262
-
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
- }
278
-
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
- }
293
-
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
- });
329
- };
330
-
331
- const main = async () => {
332
- try {
333
- const tmpDir = tmp.dirSync({ unsafeCleanup: true });
334
- const zipPath = path.join(tmpDir.name, 'archive.zip');
335
-
336
- let folderName = process.argv[2];
337
- if (folderName === '.') {
338
- folderName = path.basename(process.cwd());
339
- } else if (!folderName) {
340
- folderName = await promptForFolderName();
341
- }
342
-
343
- // Sanitize the provided folder name to prevent path traversal or absolute paths
344
- const sanitizeFolderName = (name) => {
345
- if (!name) return '';
346
- // If user provided '.', use current dir basename
347
- if (name === '.') return path.basename(process.cwd());
348
- // Disallow absolute paths
349
- if (path.isAbsolute(name)) {
350
- console.warn('Absolute paths are not allowed for destination; using basename.');
351
- name = path.basename(name);
352
- }
353
- // 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.');
356
- name = path.basename(name);
357
- }
358
- // Finally, ensure it's a single path segment
359
- name = path.basename(name);
360
- if (!name) throw new Error('Invalid destination folder name');
361
- return name;
362
- };
363
-
364
- folderName = sanitizeFolderName(folderName);
365
-
366
- const destDir = path.join(process.cwd(), folderName);
367
-
368
- if (fs.existsSync(destDir)) {
369
- const replace = await promptForReplace(destDir);
370
- if (replace) {
371
- await removeDir(destDir);
372
- await createDirIfNotExists(destDir);
373
- } else {
374
- console.log('Operation aborted.');
375
- rl.close();
376
- process.exit(0);
377
- }
378
- } else {
379
- await createDirIfNotExists(destDir);
380
- }
381
-
382
- await downloadFile(GITHUB_ZIP_URL, zipPath);
383
- console.log('Downloaded successfully.');
384
-
385
- await extractZip(zipPath, tmpDir.name);
386
-
387
- const extractedFolderName = fs.readdirSync(tmpDir.name)[0];
388
- const extractedFolderPath = path.join(tmpDir.name, extractedFolderName);
389
-
390
- fs.readdirSync(extractedFolderPath);
391
-
392
- await moveFiles(extractedFolderPath, destDir);
393
-
394
- // Remove any bundled lockfiles from the extracted archive to avoid integrity
395
- // checksum mismatches originating from the release zip's lockfile.
396
- try {
397
- await safeUnlink(path.join(destDir, 'package-lock.json')).catch(() => {});
398
- await safeUnlink(path.join(destDir, 'npm-shrinkwrap.json')).catch(() => {});
399
- } catch (e) {
400
- // ignore
401
- }
402
-
403
- await runNpmInstall(destDir);
404
-
405
- rl.close();
406
-
407
- process.exit(0);
408
- } catch (err) {
409
- console.error('Error:', err);
410
- rl.close();
411
- process.exit(1);
412
- }
413
- };
414
-
415
- main();
1
+ #!/usr/bin/env node
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";
13
+
14
+ // Configuration
15
+ const GITHUB_ZIP_URL =
16
+ "https://github.com/Asggen/afront/archive/refs/tags/v1.0.26.zip"; // Updated URL
17
+
18
+ // Define files to skip
19
+ const SKIP_FILES = [
20
+ "FUNDING.yml",
21
+ "CODE_OF_CONDUCT.md",
22
+ "SECURITY.md",
23
+ "install.js",
24
+ ".npmrc",
25
+ ];
26
+
27
+ // Initialize readline interface
28
+ const rl = readline.createInterface({
29
+ input: process.stdin,
30
+ output: process.stdout,
31
+ });
32
+
33
+ // Spinner function
34
+ const spinner = (text, delay = 100) => {
35
+ const spinnerChars = ["|", "/", "-", "\\"];
36
+ let i = 0;
37
+ const interval = setInterval(() => {
38
+ readline.cursorTo(process.stdout, 0);
39
+ process.stdout.write(`${text} ${spinnerChars[i++]}`);
40
+ i = i % spinnerChars.length;
41
+ }, delay);
42
+
43
+ return () => {
44
+ clearInterval(interval);
45
+ readline.cursorTo(process.stdout, 0);
46
+ process.stdout.write(`${text} Done.\n`);
47
+ };
48
+ };
49
+
50
+ const askQuestion = (question) => {
51
+ return new Promise((resolve) => {
52
+ rl.question(question, (answer) => {
53
+ resolve(answer.trim().toLowerCase());
54
+ });
55
+ });
56
+ };
57
+
58
+ const downloadFile = (url, destination) => {
59
+ return new Promise((resolve, reject) => {
60
+ const file = fs.createWriteStream(destination);
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);
72
+ }
73
+
74
+ if (response.statusCode !== 200) {
75
+ stopSpinner();
76
+ return reject(
77
+ new Error(
78
+ `Failed to download file. Status code: ${response.statusCode}`,
79
+ ),
80
+ );
81
+ }
82
+
83
+ response.pipe(file);
84
+
85
+ file.on("finish", () => {
86
+ file.close(() => {
87
+ stopSpinner();
88
+ resolve();
89
+ });
90
+ });
91
+ });
92
+
93
+ request.on("error", (err) => {
94
+ stopSpinner();
95
+ fs.promises.unlink(destination).catch(() => {});
96
+ reject(err);
97
+ });
98
+ });
99
+ };
100
+
101
+ const extractZip = (zipPath, extractTo) => {
102
+ return new Promise((resolve, reject) => {
103
+ const stopSpinner = spinner(chalk.yellow("šŸ“¦ Extracting files..."));
104
+ try {
105
+ const zip = new AdmZip(zipPath);
106
+ zip.extractAllTo(extractTo, true);
107
+ fs.readdir(extractTo, (err, files) => {
108
+ if (err) {
109
+ stopSpinner();
110
+ console.error("Error reading extracted folder:", err);
111
+ reject(err);
112
+ } else {
113
+ stopSpinner();
114
+ resolve();
115
+ }
116
+ });
117
+ } catch (err) {
118
+ stopSpinner();
119
+ console.error("Error extracting zip file:", err);
120
+ reject(err);
121
+ }
122
+ });
123
+ };
124
+
125
+ const runNpmInstall = (directory) => {
126
+ return new Promise((resolve, reject) => {
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) {
154
+ resolve();
155
+ } else {
156
+ console.error(chalk.red(errorOutput));
157
+ reject(new Error(`npm install failed with code ${code}`));
158
+ }
159
+ });
160
+
161
+ child.on("error", (err) => {
162
+ stopSpinner();
163
+ reject(err);
164
+ });
165
+ });
166
+ };
167
+
168
+ const createDirIfNotExists = (dirPath) => {
169
+ return new Promise((resolve, reject) => {
170
+ fs.mkdir(dirPath, { recursive: true }, (err) => {
171
+ if (err) {
172
+ reject(new Error("Error creating directory:", err));
173
+ } else {
174
+ resolve();
175
+ }
176
+ });
177
+ });
178
+ };
179
+
180
+ const isPathInside = (root, target) => {
181
+ const r = path.resolve(root);
182
+ const t = path.resolve(target);
183
+ return t === r || t.startsWith(r + path.sep);
184
+ };
185
+
186
+ // Security: path validated against SAFE_UNLINK_ROOT before unlink
187
+ const SAFE_UNLINK_ROOT = fs.realpathSync(process.cwd());
188
+
189
+ const safeUnlink = async (targetPath) => {
190
+ const resolved = await fs.promises.realpath(targetPath);
191
+
192
+ if (!resolved.startsWith(SAFE_UNLINK_ROOT + path.sep)) {
193
+ throw new Error(`Blocked unlink outside safe root: ${resolved}`);
194
+ }
195
+
196
+ const stats = await fs.promises.lstat(resolved);
197
+
198
+ if (!stats.isFile()) {
199
+ throw new Error(`Refusing to unlink non-file: ${resolved}`);
200
+ }
201
+
202
+ await fs.promises.unlink(resolved);
203
+ };
204
+
205
+ const safeRm = (dirPath, cb) => {
206
+ const resolved = path.resolve(dirPath);
207
+ if (!isPathInside(process.cwd(), resolved)) {
208
+ return cb(
209
+ new Error(
210
+ `Refusing to remove directory outside current working directory: ${resolved}`,
211
+ ),
212
+ );
213
+ }
214
+ fs.rm(resolved, { recursive: true, force: true }, cb);
215
+ };
216
+
217
+ // Perform an atomic-safe move: open source with O_NOFOLLOW, stream to a temp file, fsync, then rename
218
+ const safeMoveFile = async (resolvedSrc, resolvedDest) => {
219
+ const srcFlags =
220
+ process.platform === "win32"
221
+ ? fs.constants.O_RDONLY
222
+ : fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW;
223
+ let srcHandle;
224
+ let destHandle;
225
+ const tmpName = `${resolvedDest}.tmp-${process.pid}-${Date.now()}`;
226
+ try {
227
+ srcHandle = await fs.promises.open(resolvedSrc, srcFlags);
228
+ } catch (err) {
229
+ throw err;
230
+ }
231
+
232
+ try {
233
+ const stats = await srcHandle.stat();
234
+ if (stats.isDirectory()) {
235
+ await srcHandle.close();
236
+ throw new Error("Source is a directory");
237
+ }
238
+
239
+ await createDirIfNotExists(path.dirname(resolvedDest));
240
+
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
+ );
246
+
247
+ const bufferSize = 64 * 1024;
248
+ const buffer = Buffer.allocUnsafe(bufferSize);
249
+ let position = 0;
250
+ while (true) {
251
+ const { bytesRead } = await srcHandle.read(
252
+ buffer,
253
+ 0,
254
+ bufferSize,
255
+ position,
256
+ );
257
+ if (!bytesRead) break;
258
+ let written = 0;
259
+ while (written < bytesRead) {
260
+ const { bytesWritten } = await destHandle.write(
261
+ buffer,
262
+ written,
263
+ bytesRead - written,
264
+ );
265
+ written += bytesWritten;
266
+ }
267
+ position += bytesRead;
268
+ }
269
+
270
+ await destHandle.sync();
271
+ await destHandle.close();
272
+ await srcHandle.close();
273
+
274
+ await fs.promises.rename(tmpName, resolvedDest);
275
+ } catch (err) {
276
+ try {
277
+ if (destHandle) await destHandle.close();
278
+ } catch (e) {}
279
+ try {
280
+ await fs.promises.unlink(tmpName).catch(() => {});
281
+ } catch (e) {}
282
+ try {
283
+ if (srcHandle) await srcHandle.close();
284
+ } catch (e) {}
285
+ throw err;
286
+ }
287
+ };
288
+
289
+ const promptForFolderName = async () => {
290
+ const answer = await askQuestion(
291
+ "AFront: Enter the name of the destination folder: ",
292
+ );
293
+ return answer;
294
+ };
295
+
296
+ const promptForReplace = async (dirPath) => {
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";
301
+ };
302
+
303
+ const removeDir = (dirPath) => {
304
+ return new Promise((resolve, reject) => {
305
+ console.log(`Removing existing directory: ${dirPath}`);
306
+ safeRm(dirPath, (err) => {
307
+ if (err) return reject(err);
308
+ resolve();
309
+ });
310
+ });
311
+ };
312
+
313
+ const safeReaddir = async (dirPath, safeRoot) => {
314
+ const resolvedDir = await fs.promises.realpath(dirPath);
315
+ const resolvedRoot = await fs.promises.realpath(safeRoot);
316
+
317
+ if (
318
+ !resolvedDir.startsWith(resolvedRoot + path.sep) &&
319
+ resolvedDir !== resolvedRoot
320
+ ) {
321
+ throw new Error(`Blocked readdir outside safe root: ${resolvedDir}`);
322
+ }
323
+
324
+ const stats = await fs.promises.lstat(resolvedDir);
325
+ if (!stats.isDirectory()) {
326
+ throw new Error(`Not a directory: ${resolvedDir}`);
327
+ }
328
+
329
+ return fs.promises.readdir(resolvedDir);
330
+ };
331
+
332
+ const moveFiles = async (srcPath, destPath, safeRoot) => {
333
+ const files = await safeReaddir(srcPath, safeRoot);
334
+
335
+ for (const file of files) {
336
+ const fileName = path.basename(file);
337
+ if (SKIP_FILES.includes(fileName)) continue;
338
+
339
+ const resolvedSrc = path.resolve(srcPath, file);
340
+ const resolvedDest = path.resolve(destPath, file);
341
+
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
+ }
357
+ };
358
+
359
+ const main = async () => {
360
+ try {
361
+ const zipPath = path.join(tmpDir, "archive.zip");
362
+
363
+ let folderName = process.argv[2];
364
+ if (folderName === ".") {
365
+ folderName = path.basename(process.cwd());
366
+ } else if (!folderName) {
367
+ folderName = await promptForFolderName();
368
+ }
369
+
370
+ // Sanitize the provided folder name to prevent path traversal or absolute paths
371
+ const sanitizeFolderName = (name) => {
372
+ if (!name) return "";
373
+ // If user provided '.', use current dir basename
374
+ if (name === ".") return path.basename(process.cwd());
375
+ // Disallow absolute paths
376
+ if (path.isAbsolute(name)) {
377
+ console.warn(
378
+ "Absolute paths are not allowed for destination; using basename.",
379
+ );
380
+ name = path.basename(name);
381
+ }
382
+ // Disallow parent traversal and nested paths
383
+ if (name === ".." || name.includes(path.sep) || name.includes("..")) {
384
+ console.warn(
385
+ "Path traversal detected in destination name; using basename of input.",
386
+ );
387
+ name = path.basename(name);
388
+ }
389
+ // Finally, ensure it's a single path segment
390
+ name = path.basename(name);
391
+ if (!name) throw new Error("Invalid destination folder name");
392
+ return name;
393
+ };
394
+
395
+ folderName = sanitizeFolderName(folderName);
396
+
397
+ console.log(
398
+ chalk.green(`Creating project in ${chalk.bold(`./${folderName}`)}\n`),
399
+ );
400
+
401
+ const destDir = path.join(process.cwd(), folderName);
402
+
403
+ if (fs.existsSync(destDir)) {
404
+ const replace = await promptForReplace(destDir);
405
+ if (replace) {
406
+ await removeDir(destDir);
407
+ await createDirIfNotExists(destDir);
408
+ } else {
409
+ console.log("Operation aborted.");
410
+ rl.close();
411
+ process.exit(0);
412
+ }
413
+ } else {
414
+ await createDirIfNotExists(destDir);
415
+ }
416
+
417
+ await downloadFile(GITHUB_ZIP_URL, zipPath);
418
+
419
+ await extractZip(zipPath, tmpDir);
420
+
421
+ const extractedItems = fs.readdirSync(tmpDir);
422
+
423
+ const extractedFolderName = extractedItems.find((item) => {
424
+ const fullPath = path.join(tmpDir, item);
425
+ return fs.lstatSync(fullPath).isDirectory();
426
+ });
427
+
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);
434
+
435
+ // Remove any bundled lockfiles from the extracted archive to avoid integrity
436
+ // checksum mismatches originating from the release zip's lockfile.
437
+ try {
438
+ await safeUnlink(path.join(destDir, "package-lock.json")).catch(() => {});
439
+ await safeUnlink(path.join(destDir, "npm-shrinkwrap.json")).catch(
440
+ () => {},
441
+ );
442
+ } catch (e) {
443
+ // ignore
444
+ }
445
+
446
+ await runNpmInstall(destDir);
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
+
456
+ rl.close();
457
+
458
+ fs.rmSync(tmpDir, { recursive: true, force: true });
459
+ process.exit(0);
460
+ } catch (err) {
461
+ console.error(chalk.red("āœ– Error:"), err.message);
462
+ rl.close();
463
+ process.exit(1);
464
+ }
465
+ };
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();
518
+ main();