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.
- package/build-prod/index.html +1 -1
- package/build-prod-static/index.html +1 -1
- package/install.js +255 -152
- package/package.json +12 -6
- package/webpack.build-prod.js +1 -0
- package/webpack.prod.js +1 -0
- package/npm-shrinkwrap.json +0 -9641
package/build-prod/index.html
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<!DOCTYPE ><html data-build="prod-
|
|
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-
|
|
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(
|
|
3
|
-
const path = require(
|
|
4
|
-
const
|
|
5
|
-
const {
|
|
6
|
-
const os = require(
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
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 =
|
|
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 = [
|
|
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(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
+
|
|
85
|
+
file.on("finish", () => {
|
|
62
86
|
file.close(() => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
resolve();
|
|
66
|
-
} else {
|
|
67
|
-
stopSpinner();
|
|
68
|
-
reject(new Error('Downloaded file is empty.'));
|
|
69
|
-
}
|
|
87
|
+
stopSpinner();
|
|
88
|
+
resolve();
|
|
70
89
|
});
|
|
71
90
|
});
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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(
|
|
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 =
|
|
187
|
+
const SAFE_UNLINK_ROOT = fs.realpathSync(process.cwd());
|
|
141
188
|
|
|
142
189
|
const safeUnlink = async (targetPath) => {
|
|
143
|
-
const resolved =
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
230
|
-
|
|
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
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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(
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
280
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
|
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 ===
|
|
374
|
+
if (name === ".") return path.basename(process.cwd());
|
|
348
375
|
// Disallow absolute paths
|
|
349
376
|
if (path.isAbsolute(name)) {
|
|
350
|
-
console.warn(
|
|
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 ===
|
|
355
|
-
console.warn(
|
|
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(
|
|
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(
|
|
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
|
|
419
|
+
await extractZip(zipPath, tmpDir);
|
|
386
420
|
|
|
387
|
-
const
|
|
388
|
-
const extractedFolderPath = path.join(tmpDir.name, extractedFolderName);
|
|
421
|
+
const extractedItems = fs.readdirSync(tmpDir);
|
|
389
422
|
|
|
390
|
-
|
|
423
|
+
const extractedFolderName = extractedItems.find((item) => {
|
|
424
|
+
const fullPath = path.join(tmpDir, item);
|
|
425
|
+
return fs.lstatSync(fullPath).isDirectory();
|
|
426
|
+
});
|
|
391
427
|
|
|
392
|
-
|
|
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,
|
|
398
|
-
await safeUnlink(path.join(destDir,
|
|
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(
|
|
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();
|