create-ppob-addon 1.0.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.
- package/index.js +329 -0
- package/package.json +7 -0
package/index.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { Readable } = require("stream");
|
|
6
|
+
const { finished } = require("stream/promises");
|
|
7
|
+
const { execSync } = require("child_process");
|
|
8
|
+
const os = require("os");
|
|
9
|
+
|
|
10
|
+
// ============ š„ SERVER URL (HARDCODE) ============
|
|
11
|
+
// Ganti dengan URL server lo nanti
|
|
12
|
+
// Contoh: https://update.ppob-addon.com
|
|
13
|
+
// Atau: http://192.168.1.100:3000
|
|
14
|
+
const DEFAULT_SERVER_URL = "http://localhost:3000";
|
|
15
|
+
|
|
16
|
+
// Bisa di-override pake argumen atau env
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const argServerUrl = args.find((arg) => arg.startsWith("--server="))?.split("=")[1];
|
|
19
|
+
const envServerUrl = process.env.SERVER_URL;
|
|
20
|
+
const SERVER_URL = argServerUrl || envServerUrl || DEFAULT_SERVER_URL;
|
|
21
|
+
|
|
22
|
+
// ============ DATA ============
|
|
23
|
+
const publicKeyData = `-----BEGIN PUBLIC KEY-----
|
|
24
|
+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApXG04gRyR71EXKdUxjve
|
|
25
|
+
ol+/eCZOof0bl1rZMlAhO9UtYifqqnSYDyNCalwdQ9z3+pW2xOVbrEUfsIGR+Fqz
|
|
26
|
+
0U+OTGphtai6uJmiVtErM+geuTp/sGUH3l9EfvZAMfnywK+OgXEITTWq3BQE8eRt
|
|
27
|
+
/Y0rTFPJtOzGWLFZ3kYu9ZdjPaXEI979Bz1l0tO5PLG/C84Q3GT90pcQPjLJJ5vd
|
|
28
|
+
xM17zm0zhJN6UG2tU+KXlVZAbvzffv/uFY5K1Mvp5ckaAwteAV/KREMzBZgReqyn
|
|
29
|
+
glXcB7GpaQh4QuXbona2UQqkNQhXU/ExXdI9Oo76KNvfEZIGAIrA3HEEsEOrpqfw
|
|
30
|
+
3QIDAQAB
|
|
31
|
+
-----END PUBLIC KEY-----`;
|
|
32
|
+
|
|
33
|
+
// š„ .env akan di-generate dengan SERVER_URL yang dipake
|
|
34
|
+
const envData = `# ==================== SERVER ====================
|
|
35
|
+
PORT=5000
|
|
36
|
+
NODE_ENV=production
|
|
37
|
+
|
|
38
|
+
# ==================== UPDATE SERVER ====================
|
|
39
|
+
SERVER_URL=${SERVER_URL}
|
|
40
|
+
SERVER_WS_URL=${SERVER_URL.replace("http://", "ws://").replace("https://", "wss://")}
|
|
41
|
+
|
|
42
|
+
# ==================== REDIS ====================
|
|
43
|
+
REDIS_HOST=localhost
|
|
44
|
+
REDIS_PORT=6379
|
|
45
|
+
REDIS_PASSWORD=
|
|
46
|
+
|
|
47
|
+
# ==================== IP WHITELIST ====================
|
|
48
|
+
ALLOWED_IPS=127.0.0.1
|
|
49
|
+
|
|
50
|
+
# ==================== LOGGING ====================
|
|
51
|
+
LOG_LEVEL=info
|
|
52
|
+
LOG_FILE=./logs/app.log`;
|
|
53
|
+
|
|
54
|
+
const ecosystemConfigData = `module.exports = {
|
|
55
|
+
apps: [
|
|
56
|
+
{
|
|
57
|
+
name: "ppob_addon",
|
|
58
|
+
script: "current/index.js",
|
|
59
|
+
instances: 4,
|
|
60
|
+
exec_mode: "cluster",
|
|
61
|
+
watch: false,
|
|
62
|
+
ignore_watch: ["node_modules", "logs", ".git"],
|
|
63
|
+
error_file: "./logs/err.log",
|
|
64
|
+
out_file: "./logs/out.log",
|
|
65
|
+
log_file: "./logs/combined.log",
|
|
66
|
+
time: true,
|
|
67
|
+
max_memory_restart: "1G",
|
|
68
|
+
min_uptime: "10s",
|
|
69
|
+
max_restarts: 10,
|
|
70
|
+
restart_delay: 4000,
|
|
71
|
+
env: {
|
|
72
|
+
NODE_ENV: "production",
|
|
73
|
+
PORT: 5000,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
};`;
|
|
78
|
+
|
|
79
|
+
const packageJsonData = `{
|
|
80
|
+
"name": "ppob_addon",
|
|
81
|
+
"scripts": {
|
|
82
|
+
"pm2:start": "pm2 start ecosystem.config.js --env production",
|
|
83
|
+
"pm2:stop": "pm2 stop ppob_addon",
|
|
84
|
+
"pm2:delete": "pm2 delete ppob_addon",
|
|
85
|
+
"pm2:restart": "pm2 restart ppob_addon",
|
|
86
|
+
"pm2:reload": "pm2 reload ecosystem.config.js --update-env",
|
|
87
|
+
"pm2:reset": "pm2 delete ppob_addon && pm2 start ecosystem.config.js && pm2 save",
|
|
88
|
+
"pm2:logs": "pm2 logs ppob_addon",
|
|
89
|
+
"pm2:monit": "pm2 monit"
|
|
90
|
+
}
|
|
91
|
+
}`;
|
|
92
|
+
|
|
93
|
+
// ============ FUNCTIONS ============
|
|
94
|
+
async function download() {
|
|
95
|
+
console.log(`ā¬ļø Downloading from ${SERVER_URL}/api/update/download...`);
|
|
96
|
+
const response = await fetch(`${SERVER_URL}/api/update/download`);
|
|
97
|
+
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
throw new Error(`Failed to fetch: ${response.statusText}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const contentDisposition = response.headers.get("content-disposition");
|
|
103
|
+
if (!contentDisposition) {
|
|
104
|
+
throw new Error("No content-disposition header");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const match = contentDisposition.match(/filename="(.*?)"/);
|
|
108
|
+
if (!match) {
|
|
109
|
+
throw new Error("No filename in content-disposition");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const fileName = match[1];
|
|
113
|
+
const [, name, version] = fileName.match(/(\w+)-([0-9.]+)\.zip/);
|
|
114
|
+
|
|
115
|
+
const parentPath = path.join("releases");
|
|
116
|
+
const filePath = path.join(parentPath, fileName);
|
|
117
|
+
|
|
118
|
+
if (!fs.existsSync(parentPath)) {
|
|
119
|
+
fs.mkdirSync(parentPath, { recursive: true });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const destination = fs.createWriteStream(filePath);
|
|
123
|
+
const nodeReadable = Readable.fromWeb(response.body);
|
|
124
|
+
nodeReadable.pipe(destination);
|
|
125
|
+
await finished(destination);
|
|
126
|
+
|
|
127
|
+
console.log(`ā
Download complete: ${fileName}`);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
success: true,
|
|
131
|
+
message: "Download completed successfully.",
|
|
132
|
+
data: {
|
|
133
|
+
name: fileName,
|
|
134
|
+
version,
|
|
135
|
+
filePath,
|
|
136
|
+
parentPath,
|
|
137
|
+
targetPath: filePath.replace(/\.zip$/, ""),
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function unzipFile(zipPath, targetPath) {
|
|
143
|
+
const isWindows = os.platform() === "win32";
|
|
144
|
+
console.log(`š¦ Extracting ${path.basename(zipPath)}...`);
|
|
145
|
+
|
|
146
|
+
if (isWindows) {
|
|
147
|
+
try {
|
|
148
|
+
execSync(`powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${targetPath}' -Force"`);
|
|
149
|
+
} catch {
|
|
150
|
+
console.log("ā ļø PowerShell failed, trying 7zip...");
|
|
151
|
+
execSync(`7z x ${zipPath} -o${targetPath} -y`);
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
execSync(`unzip -o ${zipPath} -d ${targetPath}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log(`ā
Extraction complete: ${targetPath}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function createSymlink(targetPath, linkPath) {
|
|
161
|
+
console.log(`š Creating symlink: ${linkPath} ā ${targetPath}`);
|
|
162
|
+
|
|
163
|
+
if (fs.existsSync(linkPath)) {
|
|
164
|
+
fs.unlinkSync(linkPath);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fs.symlinkSync(targetPath, linkPath, "junction");
|
|
168
|
+
console.log(`ā
Symlink created`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function checkPrerequisites() {
|
|
172
|
+
console.log("š Checking prerequisites...");
|
|
173
|
+
|
|
174
|
+
// 1. Cek Node.js
|
|
175
|
+
try {
|
|
176
|
+
const nodeVersion = execSync("node --version", { encoding: "utf8" }).trim();
|
|
177
|
+
const requiredVersion = "v24.11.1";
|
|
178
|
+
|
|
179
|
+
if (nodeVersion !== requiredVersion) {
|
|
180
|
+
console.error(`ā Node.js version mismatch!`);
|
|
181
|
+
console.error(` Required: ${requiredVersion}`);
|
|
182
|
+
console.error(` Current: ${nodeVersion}`);
|
|
183
|
+
console.error(`\nš„ Download Node.js ${requiredVersion} from:`);
|
|
184
|
+
console.error(` https://nodejs.org/dist/v24.11.1/`);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
console.log(`ā
Node.js ${nodeVersion}`);
|
|
188
|
+
} catch {
|
|
189
|
+
console.error("ā Node.js is not installed!");
|
|
190
|
+
console.error("š„ Download from: https://nodejs.org/dist/v24.11.1/");
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 2. Cek PM2
|
|
195
|
+
try {
|
|
196
|
+
const pm2Version = execSync("pm2 --version", { encoding: "utf8" }).trim();
|
|
197
|
+
console.log(`ā
PM2 ${pm2Version}`);
|
|
198
|
+
} catch {
|
|
199
|
+
console.log("ā ļø PM2 not found, installing globally...");
|
|
200
|
+
execSync("npm install -g pm2", { stdio: "inherit" });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 3. Cek unzip (Linux/Mac)
|
|
204
|
+
if (os.platform() !== "win32") {
|
|
205
|
+
try {
|
|
206
|
+
execSync("which unzip", { stdio: "ignore" });
|
|
207
|
+
console.log("ā
unzip command found");
|
|
208
|
+
} catch {
|
|
209
|
+
console.error("ā unzip command not found!");
|
|
210
|
+
console.log("Install with: sudo apt-get install unzip");
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// š„ 4. Cek Redis/Memurai
|
|
216
|
+
console.log("š Checking Redis/Memurai...");
|
|
217
|
+
const isWindows = os.platform() === "win32";
|
|
218
|
+
let redisOk = false;
|
|
219
|
+
|
|
220
|
+
if (isWindows) {
|
|
221
|
+
try {
|
|
222
|
+
const result = execSync("sc query Memurai", { encoding: "utf8" });
|
|
223
|
+
if (result.includes("RUNNING")) {
|
|
224
|
+
console.log("ā
Memurai is running");
|
|
225
|
+
redisOk = true;
|
|
226
|
+
} else {
|
|
227
|
+
console.log("ā ļø Memurai installed but not running");
|
|
228
|
+
console.log(" Run: net start Memurai");
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
console.log("ā ļø Memurai not found, checking port 6379...");
|
|
232
|
+
// Cek port 6379 (mungkin Redis jalan lewat Docker)
|
|
233
|
+
try {
|
|
234
|
+
execSync("netstat -ano | findstr :6379", { encoding: "utf8", stdio: "ignore" });
|
|
235
|
+
console.log("ā
Port 6379 is open (Redis is running)");
|
|
236
|
+
redisOk = true;
|
|
237
|
+
} catch {
|
|
238
|
+
console.log("ā Redis/Memurai not found!");
|
|
239
|
+
console.log("š„ Install Memurai: https://www.memurai.com/get-memurai");
|
|
240
|
+
console.log(" Or run Redis via Docker:");
|
|
241
|
+
console.log(" docker run -d --name redis -p 6379:6379 redis");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
try {
|
|
246
|
+
const result = execSync("redis-cli ping", { encoding: "utf8" });
|
|
247
|
+
if (result.includes("PONG")) {
|
|
248
|
+
console.log("ā
Redis is running");
|
|
249
|
+
redisOk = true;
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
try {
|
|
253
|
+
execSync("netstat -tlnp | grep :6379", { encoding: "utf8", stdio: "ignore" });
|
|
254
|
+
console.log("ā
Port 6379 is open (Redis is running)");
|
|
255
|
+
redisOk = true;
|
|
256
|
+
} catch {
|
|
257
|
+
console.log("ā Redis not found!");
|
|
258
|
+
console.log("š„ Install: sudo apt-get install redis-server");
|
|
259
|
+
console.log(" Or run via Docker: docker run -d --name redis -p 6379:6379 redis");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 5. Kasih warning kalo Redis nggak jalan
|
|
265
|
+
if (!redisOk) {
|
|
266
|
+
console.log("\nā ļø WARNING: Redis/Memurai is not running!");
|
|
267
|
+
console.log(" The app will still run but may have issues with:");
|
|
268
|
+
console.log(" - Session management");
|
|
269
|
+
console.log(" - Rate limiting");
|
|
270
|
+
console.log(" - Caching");
|
|
271
|
+
console.log("\n Please install and start Redis/Memurai before running the app.\n");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
console.log("ā
All prerequisites satisfied");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function install() {
|
|
278
|
+
console.log("\nš PPOB_ADDON Installer\n");
|
|
279
|
+
console.log(`š” Server URL: ${SERVER_URL}\n`);
|
|
280
|
+
|
|
281
|
+
checkPrerequisites();
|
|
282
|
+
|
|
283
|
+
console.log("\nš Generating configuration files...");
|
|
284
|
+
const resources = [
|
|
285
|
+
["keys/public.key", publicKeyData],
|
|
286
|
+
[".env", envData],
|
|
287
|
+
["ecosystem.config.js", ecosystemConfigData],
|
|
288
|
+
["package.json", packageJsonData],
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
for (const [file, data] of resources) {
|
|
292
|
+
if (fs.existsSync(file)) {
|
|
293
|
+
console.log(`āļø Skipping ${file} (already exists)`);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const dir = path.dirname(file);
|
|
298
|
+
if (!fs.existsSync(dir)) {
|
|
299
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
300
|
+
}
|
|
301
|
+
fs.writeFileSync(file, data);
|
|
302
|
+
console.log(`ā
Created ${file}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
console.log("\nā¬ļø Downloading latest version...");
|
|
306
|
+
const downloadResult = await download();
|
|
307
|
+
if (!downloadResult.success) {
|
|
308
|
+
console.error("ā Download failed:", downloadResult);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const { filePath, targetPath } = downloadResult.data;
|
|
313
|
+
unzipFile(filePath, targetPath);
|
|
314
|
+
createSymlink(targetPath, path.join("current"));
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
console.log("\nš Installation complete!\n");
|
|
318
|
+
console.log("š Next steps:");
|
|
319
|
+
console.log(" 1. Edit .env if needed");
|
|
320
|
+
console.log(" 2. Run: npm run pm2:start");
|
|
321
|
+
console.log(" 3. Check: npm run pm2:status");
|
|
322
|
+
console.log("\nš” All dependencies are bundled, no need to run npm install!");
|
|
323
|
+
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
install().catch((error) => {
|
|
327
|
+
console.error("ā Installation failed:", error.message);
|
|
328
|
+
process.exit(1);
|
|
329
|
+
});
|