lapeeh 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.
Files changed (76) hide show
  1. package/.env.example +14 -0
  2. package/LICENSE +21 -0
  3. package/bin/index.js +934 -0
  4. package/doc/en/ARCHITECTURE_GUIDE.md +79 -0
  5. package/doc/en/CHANGELOG.md +203 -0
  6. package/doc/en/CHEATSHEET.md +90 -0
  7. package/doc/en/CLI.md +111 -0
  8. package/doc/en/CONTRIBUTING.md +119 -0
  9. package/doc/en/DEPLOYMENT.md +171 -0
  10. package/doc/en/FAQ.md +69 -0
  11. package/doc/en/FEATURES.md +99 -0
  12. package/doc/en/GETTING_STARTED.md +84 -0
  13. package/doc/en/INTRODUCTION.md +62 -0
  14. package/doc/en/PACKAGES.md +63 -0
  15. package/doc/en/PERFORMANCE.md +98 -0
  16. package/doc/en/ROADMAP.md +104 -0
  17. package/doc/en/SECURITY.md +95 -0
  18. package/doc/en/STRUCTURE.md +79 -0
  19. package/doc/en/TUTORIAL.md +145 -0
  20. package/doc/id/ARCHITECTURE_GUIDE.md +76 -0
  21. package/doc/id/CHANGELOG.md +203 -0
  22. package/doc/id/CHEATSHEET.md +90 -0
  23. package/doc/id/CLI.md +139 -0
  24. package/doc/id/CONTRIBUTING.md +119 -0
  25. package/doc/id/DEPLOYMENT.md +171 -0
  26. package/doc/id/FAQ.md +69 -0
  27. package/doc/id/FEATURES.md +169 -0
  28. package/doc/id/GETTING_STARTED.md +91 -0
  29. package/doc/id/INTRODUCTION.md +62 -0
  30. package/doc/id/PACKAGES.md +63 -0
  31. package/doc/id/PERFORMANCE.md +100 -0
  32. package/doc/id/ROADMAP.md +107 -0
  33. package/doc/id/SECURITY.md +94 -0
  34. package/doc/id/STRUCTURE.md +79 -0
  35. package/doc/id/TUTORIAL.md +145 -0
  36. package/docker-compose.yml +24 -0
  37. package/ecosystem.config.js +17 -0
  38. package/eslint.config.mjs +26 -0
  39. package/gitignore.template +30 -0
  40. package/lib/bootstrap.ts +210 -0
  41. package/lib/core/realtime.ts +34 -0
  42. package/lib/core/redis.ts +139 -0
  43. package/lib/core/serializer.ts +63 -0
  44. package/lib/core/server.ts +70 -0
  45. package/lib/core/store.ts +116 -0
  46. package/lib/middleware/auth.ts +63 -0
  47. package/lib/middleware/error.ts +50 -0
  48. package/lib/middleware/multipart.ts +13 -0
  49. package/lib/middleware/rateLimit.ts +14 -0
  50. package/lib/middleware/requestLogger.ts +27 -0
  51. package/lib/middleware/visitor.ts +178 -0
  52. package/lib/utils/logger.ts +100 -0
  53. package/lib/utils/pagination.ts +56 -0
  54. package/lib/utils/response.ts +88 -0
  55. package/lib/utils/validator.ts +394 -0
  56. package/nodemon.json +6 -0
  57. package/package.json +126 -0
  58. package/readme.md +357 -0
  59. package/scripts/check-update.js +92 -0
  60. package/scripts/config-clear.js +45 -0
  61. package/scripts/generate-jwt-secret.js +38 -0
  62. package/scripts/init-project.js +84 -0
  63. package/scripts/make-module.js +89 -0
  64. package/scripts/release.js +494 -0
  65. package/scripts/seed-json.js +158 -0
  66. package/scripts/verify-rbac-functional.js +187 -0
  67. package/src/config/app.ts +9 -0
  68. package/src/config/cors.ts +5 -0
  69. package/src/modules/Auth/auth.controller.ts +519 -0
  70. package/src/modules/Rbac/rbac.controller.ts +533 -0
  71. package/src/routes/auth.ts +74 -0
  72. package/src/routes/index.ts +7 -0
  73. package/src/routes/rbac.ts +42 -0
  74. package/storage/logs/.gitkeep +0 -0
  75. package/tsconfig.build.json +12 -0
  76. package/tsconfig.json +30 -0
@@ -0,0 +1,89 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const args = process.argv.slice(2);
5
+ const moduleName = args[0];
6
+
7
+ if (!moduleName) {
8
+ console.error('āŒ Please specify the module name.');
9
+ console.error(' Usage: npm run make:module <ModuleName>');
10
+ process.exit(1);
11
+ }
12
+
13
+ // Capitalize first letter
14
+ const name = moduleName.charAt(0).toUpperCase() + moduleName.slice(1);
15
+ const lowerName = moduleName.toLowerCase();
16
+
17
+ const moduleDir = path.join(__dirname, '..', 'src', 'modules', name);
18
+
19
+ if (fs.existsSync(moduleDir)) {
20
+ console.error(`āŒ Module ${name} already exists at ${moduleDir}`);
21
+ process.exit(1);
22
+ }
23
+
24
+ fs.mkdirSync(moduleDir, { recursive: true });
25
+
26
+ // Controller
27
+ const controllerContent = `import { Request, Response } from "express";
28
+ import { sendSuccess } from "@lapeeh/utils/response";
29
+ // import * as ${name}Service from "./${lowerName}.service";
30
+
31
+ export async function index(_req: Request, res: Response) {
32
+ sendSuccess(res, 200, "Index ${name}");
33
+ }
34
+
35
+ export async function show(req: Request, res: Response) {
36
+ const { id } = req.params;
37
+ sendSuccess(res, 200, "Show ${name} " + id);
38
+ }
39
+
40
+ export async function create(_req: Request, res: Response) {
41
+ sendSuccess(res, 201, "Create ${name}");
42
+ }
43
+
44
+ export async function update(req: Request, res: Response) {
45
+ const { id } = req.params;
46
+ sendSuccess(res, 200, "Update ${name} " + id);
47
+ }
48
+
49
+ export async function destroy(req: Request, res: Response) {
50
+ const { id } = req.params;
51
+ sendSuccess(res, 200, "Delete ${name} " + id);
52
+ }
53
+ `;
54
+
55
+ fs.writeFileSync(path.join(moduleDir, `${lowerName}.controller.ts`), controllerContent);
56
+
57
+ // Service (Optional but good for NestJS style)
58
+ const serviceContent = `
59
+ export async function findAll() {
60
+ return [];
61
+ }
62
+
63
+ export async function findOne(_id: number) {
64
+ return null;
65
+ }
66
+ `;
67
+ fs.writeFileSync(path.join(moduleDir, `${lowerName}.service.ts`), serviceContent);
68
+
69
+ // Route Stub
70
+ const routeContent = `import { Router } from "express";
71
+ import * as ${name}Controller from "./${lowerName}.controller";
72
+
73
+ const router = Router();
74
+
75
+ router.get("/", ${name}Controller.index);
76
+ router.get("/:id", ${name}Controller.show);
77
+ router.post("/", ${name}Controller.create);
78
+ router.put("/:id", ${name}Controller.update);
79
+ router.delete("/:id", ${name}Controller.destroy);
80
+
81
+ export default router;
82
+ `;
83
+ fs.writeFileSync(path.join(moduleDir, `${lowerName}.routes.ts`), routeContent);
84
+
85
+ console.log(`āœ… Module ${name} created successfully at src/modules/${name}`);
86
+ console.log(` - ${lowerName}.controller.ts`);
87
+ console.log(` - ${lowerName}.service.ts`);
88
+ console.log(` - ${lowerName}.routes.ts`);
89
+ console.log(`\nšŸ‘‰ Don't forget to register the route in src/routes/index.ts!`);
@@ -0,0 +1,494 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const readline = require('readline');
4
+ const { execSync } = require('child_process');
5
+
6
+ const rl = readline.createInterface({
7
+ input: process.stdin,
8
+ output: process.stdout
9
+ });
10
+
11
+ const question = (query) => new Promise((resolve) => rl.question(query, resolve));
12
+
13
+ // Paths
14
+ const rootDir = path.resolve(__dirname, '..');
15
+ const websiteDir = path.join(rootDir, 'website');
16
+ const packageJsonPath = path.join(rootDir, 'package.json');
17
+ const websitePackageJsonPath = path.join(websiteDir, 'package.json');
18
+
19
+ // Read current version
20
+ const pkg = require(packageJsonPath);
21
+ const currentVersion = pkg.version;
22
+
23
+ console.log(`\nšŸš€ lapeeh Release Automation Script`);
24
+ console.log(`Current Version: ${currentVersion}\n`);
25
+
26
+ // Helper to get git changes
27
+ function getGitChanges() {
28
+ try {
29
+ // Try to find the last tag
30
+ let lastTag = '';
31
+ try {
32
+ lastTag = execSync('git describe --tags --abbrev=0', { stdio: 'pipe' }).toString().trim();
33
+ } catch (e) {
34
+ // No tags found, maybe fetch all commits
35
+ lastTag = '';
36
+ }
37
+
38
+ const range = lastTag ? `${lastTag}..HEAD` : 'HEAD';
39
+ const logs = execSync(`git log ${range} --pretty=format:"%s"`, { stdio: 'pipe' }).toString().trim();
40
+
41
+ if (!logs) return [];
42
+
43
+ // Filter out chores, merges, etc. if desired, or keep everything
44
+ return logs.split('\n')
45
+ .map(l => l.trim())
46
+ .filter(l =>
47
+ l &&
48
+ !l.startsWith('chore:') &&
49
+ !l.startsWith('Merge branch') &&
50
+ !l.startsWith('docs: release') &&
51
+ !l.includes('release v') &&
52
+ !l.includes('Update version')
53
+ );
54
+ } catch (e) {
55
+ return [];
56
+ }
57
+ }
58
+
59
+ // Helper to get npm version
60
+ function getNpmVersion() {
61
+ try {
62
+ return execSync('npm view lapeeh version', { stdio: 'pipe' }).toString().trim();
63
+ } catch (e) {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ // Helper to increment patch version
69
+ function incrementPatch(version) {
70
+ const parts = version.split('.').map(Number);
71
+ if (parts.length !== 3 || parts.some(isNaN)) return version; // Fallback
72
+ parts[2]++;
73
+ return parts.join('.');
74
+ }
75
+
76
+ // Helper to generate auto commit message
77
+ function generateAutoCommitMessage() {
78
+ try {
79
+ const status = execSync('git status --porcelain', { stdio: 'pipe' }).toString().trim();
80
+ if (!status) return 'chore: no changes detected';
81
+
82
+ const files = status.split('\n').map(line => line.substring(3).trim());
83
+
84
+ const hasDocs = files.some(f => f.startsWith('website/') || f.startsWith('doc/') || f.endsWith('.md'));
85
+ const hasScripts = files.some(f => f.startsWith('scripts/'));
86
+ const hasPackage = files.some(f => f.includes('package.json'));
87
+ const hasSrc = files.some(f => !f.startsWith('website/') && !f.startsWith('doc/') && !f.startsWith('scripts/') && !f.includes('package.json') && !f.startsWith('.'));
88
+
89
+ let types = [];
90
+ if (hasDocs) types.push('docs');
91
+ if (hasScripts) types.push('scripts');
92
+ if (hasPackage) types.push('deps');
93
+ if (hasSrc) types.push('feat/fix');
94
+
95
+ if (types.length === 0) return 'chore: update project files';
96
+
97
+ if (hasDocs && !hasSrc && !hasScripts) return 'docs: update documentation';
98
+ if (hasScripts && !hasSrc) return 'chore: update build scripts';
99
+
100
+ return `chore: update ${types.join(', ')}`;
101
+ } catch (e) {
102
+ return 'chore: update project files';
103
+ }
104
+ }
105
+
106
+ // Helper to parse changelog entry with structure
107
+ function parseChangelogEntry(filePath, version) {
108
+ try {
109
+ if (!fs.existsSync(filePath)) return null;
110
+ const content = fs.readFileSync(filePath, 'utf8');
111
+
112
+ // 1. Find the header line to extract Title
113
+ // Regex matches: ## [Date] - Day, Date - Title (vVersion)
114
+ const headerRegex = new RegExp(`## \\[.*?\\] - .*? - (.*?) \\(v${version}\\)`, 'i');
115
+ const headerMatch = content.match(headerRegex);
116
+ const title = headerMatch ? headerMatch[1].trim() : null;
117
+
118
+ // 2. Extract the body
119
+ const bodyRegex = new RegExp(`## \\[.*?\\] - .*?v${version}.*?([\\s\\S]*?)(?=\\n## \\[|$)`, 'i');
120
+ const bodyMatch = content.match(bodyRegex);
121
+
122
+ if (!bodyMatch) return null;
123
+
124
+ let rawBody = bodyMatch[1].trim();
125
+
126
+ // 3. Extract Intro (text before first ###)
127
+ let intro = '';
128
+ let features = rawBody;
129
+
130
+ const firstHeaderIndex = rawBody.indexOf('###');
131
+ if (firstHeaderIndex > 0) {
132
+ intro = rawBody.substring(0, firstHeaderIndex).trim();
133
+ features = rawBody.substring(firstHeaderIndex).trim();
134
+ } else if (firstHeaderIndex === -1 && !rawBody.startsWith('-') && !rawBody.startsWith('*')) {
135
+ // If no subheaders and doesn't start with list, treat as intro
136
+ intro = rawBody;
137
+ features = '';
138
+ }
139
+
140
+ return {
141
+ title,
142
+ intro,
143
+ features
144
+ };
145
+ } catch (e) {
146
+ return null;
147
+ }
148
+ }
149
+
150
+ async function main() {
151
+ try {
152
+ // 0. Quick Git Update Check
153
+ const quickGit = await question('\n⚔ Apakah ini hanya Quick Git Update (tanpa rilis versi)? (y/n): ');
154
+ if (quickGit.toLowerCase() === 'y') {
155
+ console.log('\nšŸ¤– Auto-generating commit message...');
156
+ const commitMsg = generateAutoCommitMessage();
157
+ console.log(`šŸ“ Commit Message: "${commitMsg}"`);
158
+
159
+ try {
160
+ console.log('šŸ”„ Syncing documentation (just in case)...');
161
+ try {
162
+ execSync('node scripts/sync-docs.js', { cwd: websiteDir, stdio: 'inherit' });
163
+ } catch (e) {
164
+ console.log('āš ļø Warning: Doc sync failed, continuing...');
165
+ }
166
+
167
+ execSync('git add .', { stdio: 'inherit' });
168
+ execSync(`git commit -m "${commitMsg}"`, { stdio: 'inherit' });
169
+ execSync('git push origin HEAD', { stdio: 'inherit' });
170
+ console.log('āœ… Quick Git Update selesai!');
171
+ process.exit(0);
172
+ } catch (e) {
173
+ console.error('āŒ Error during git operations:', e.message);
174
+ process.exit(1);
175
+ }
176
+ }
177
+
178
+ // 1. Check versions and ask for new one
179
+ console.log('šŸ” Checking npm version...');
180
+ const npmVersion = getNpmVersion();
181
+ const baseVersion = npmVersion || currentVersion;
182
+ const suggestedVersion = incrementPatch(baseVersion);
183
+
184
+ console.log(`Latest npm version: ${npmVersion || 'Not found (using local)'}`);
185
+ console.log(`Current local version: ${currentVersion}`);
186
+
187
+ const newVersionInput = await question(`Enter new version (default: ${suggestedVersion}): `);
188
+ const newVersion = newVersionInput.trim() || suggestedVersion;
189
+
190
+ if (!newVersion) {
191
+ console.log('āŒ Version is required');
192
+ process.exit(1);
193
+ }
194
+
195
+ // Always update package.json locally first
196
+ console.log('\nšŸ“¦ Updating package.json files...');
197
+ updatePackageJson(packageJsonPath, newVersion);
198
+ updatePackageJson(websitePackageJsonPath, newVersion);
199
+
200
+ // 2. Question: Blog
201
+ const createBlog = await question('\n1. Apa Anda akan membuatkan blog untuk fitur baru ini? (y/n): ');
202
+ let blogTitleEN = '';
203
+
204
+ if (createBlog.toLowerCase() === 'y') {
205
+
206
+ const useAuto = await question('šŸ¤– Auto-generate content from CHANGELOG/Git? (y/n): ');
207
+
208
+ let titleID, descriptionID, introID, featureListID;
209
+ let titleEN, descriptionEN, introEN, featureListEN;
210
+
211
+ if (useAuto.toLowerCase() === 'y') {
212
+ console.log('\nšŸ¤– Auto-detecting changes from Git & Changelog...');
213
+ const changes = getGitChanges();
214
+
215
+ // Try to read from CHANGELOG.md first
216
+ const parsedID = parseChangelogEntry(path.join(rootDir, 'doc/id/CHANGELOG.md'), newVersion);
217
+ const parsedEN = parseChangelogEntry(path.join(rootDir, 'doc/en/CHANGELOG.md'), newVersion);
218
+
219
+ if (parsedID) {
220
+ console.log('āœ… Found entry in doc/id/CHANGELOG.md');
221
+ titleID = parsedID.title || `Update Terbaru v${newVersion}`;
222
+ introID = parsedID.intro || `Kami dengan bangga mengumumkan rilis **lapeeh v${newVersion}**. Update ini menghadirkan **${parsedID.title || 'berbagai fitur baru'}** untuk meningkatkan pengalaman pengembangan Anda.`;
223
+ descriptionID = parsedID.intro ? parsedID.intro.split('\n')[0] : `Rilis versi ${newVersion} hadir dengan berbagai pembaruan dan perbaikan.`;
224
+ featureListID = parsedID.features;
225
+ } else {
226
+ console.log('āš ļø No entry in doc/id/CHANGELOG.md, using git logs...');
227
+ titleID = changes.length > 0 ? changes[0] : 'Maintenance Release';
228
+ descriptionID = changes.length > 0 ? `Includes: ${changes.slice(0, 2).join(', ')}` : 'Routine maintenance and updates.';
229
+ introID = `Kami dengan bangga mengumumkan rilis **lapeeh v${newVersion}**. Rilis ini mencakup pemeliharaan rutin dan perbaikan bug.`;
230
+ featureListID = changes.length > 0
231
+ ? changes.map(f => `* **${f.trim()}**`).join('\n')
232
+ : '* **Routine maintenance**';
233
+ }
234
+
235
+ if (parsedEN) {
236
+ console.log('āœ… Found entry in doc/en/CHANGELOG.md');
237
+ titleEN = parsedEN.title || `Latest Update v${newVersion}`;
238
+ introEN = parsedEN.intro || `We are proud to announce the release of **lapeeh v${newVersion}**. This update brings **${parsedEN.title || 'various new features'}** to enhance your development experience.`;
239
+ descriptionEN = parsedEN.intro ? parsedEN.intro.split('\n')[0] : `Release version ${newVersion} comes with various updates and improvements.`;
240
+ featureListEN = parsedEN.features;
241
+ } else {
242
+ console.log('āš ļø No entry in doc/en/CHANGELOG.md, using git logs...');
243
+ titleEN = changes.length > 0 ? changes[0] : 'Maintenance Release';
244
+ descriptionEN = changes.length > 0 ? `Includes: ${changes.slice(0, 2).join(', ')}` : 'Routine maintenance and updates.';
245
+ introEN = `We are proud to announce the release of **lapeeh v${newVersion}**. This release includes routine maintenance and bug fixes.`;
246
+ featureListEN = changes.length > 0
247
+ ? changes.map(f => `* **${f.trim()}**`).join('\n')
248
+ : '* **Routine maintenance**';
249
+ }
250
+ } else {
251
+ console.log('\nšŸ“ Manual Blog Entry');
252
+ console.log('Silakan masukkan detail blog secara manual.');
253
+
254
+ // ID Inputs
255
+ titleID = await question('Judul Blog (ID): ');
256
+ descriptionID = await question('Deskripsi Singkat (ID): ');
257
+ const contentID = await question('Konten Utama/Fitur (ID) - Gunakan format Markdown jika perlu: ');
258
+ introID = `Rilis versi ${newVersion} telah hadir.`; // Fallback for manual
259
+ featureListID = contentID;
260
+
261
+ console.log('\n--- English Version ---');
262
+
263
+ // EN Inputs
264
+ titleEN = await question('Blog Title (EN): ');
265
+ descriptionEN = await question('Short Description (EN): ');
266
+ const contentEN = await question('Main Content/Features (EN): ');
267
+ introEN = `Release version ${newVersion} is here.`; // Fallback for manual
268
+ featureListEN = contentEN;
269
+
270
+ // Set defaults if empty
271
+ if (!titleID) titleID = `Update v${newVersion}`;
272
+ if (!descriptionID) descriptionID = `Pembaruan versi ${newVersion}`;
273
+ if (!featureListID) featureListID = '* Pembaruan rutin';
274
+
275
+ if (!titleEN) titleEN = `Update v${newVersion}`;
276
+ if (!descriptionEN) descriptionEN = `Update version ${newVersion}`;
277
+ if (!featureListEN) featureListEN = '* Routine updates';
278
+ }
279
+
280
+ blogTitleEN = titleEN; // Save for commit message
281
+
282
+ console.log('šŸ“ Generating blog posts...');
283
+ const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
284
+ const dateString = new Date().toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' });
285
+ const dateStringEn = new Date().toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
286
+
287
+ const blogFileName = `release-v${newVersion}.md`;
288
+
289
+ // Indonesian Blog Content
290
+ const idContent = `---
291
+ title: "Rilis lapeeh v${newVersion}: ${titleID}"
292
+ date: ${date}
293
+ author: Tim lapeeh
294
+ description: "${descriptionID.replace(/"/g, '\\"')}"
295
+ ---
296
+
297
+ # Rilis lapeeh v${newVersion}: ${titleID}
298
+
299
+ Ditulis pada **${dateString}** oleh **Tim lapeeh**
300
+
301
+ ${introID}
302
+
303
+ ## Apa yang Baru? šŸš€
304
+
305
+ ${featureListID}
306
+
307
+ ## Cara Upgrade
308
+
309
+ Bagi pengguna baru, cukup jalankan:
310
+
311
+ \`\`\`bash
312
+ npx lapeeh init my-project
313
+ \`\`\`
314
+
315
+ Bagi pengguna lama yang ingin update ke versi terbaru:
316
+
317
+ \`\`\`bash
318
+ npm install lapeeh@latest
319
+ \`\`\`
320
+
321
+ Terima kasih telah menjadi bagian dari perjalanan lapeeh Framework!
322
+ `;
323
+
324
+ // English Blog Content
325
+ const enContent = `---
326
+ title: "Release lapeeh v${newVersion}: ${titleEN}"
327
+ date: ${date}
328
+ author: lapeeh Team
329
+ description: "${descriptionEN.replace(/"/g, '\\"')}"
330
+ ---
331
+
332
+ # Release lapeeh v${newVersion}: ${titleEN}
333
+
334
+ Written on **${dateStringEn}** by **lapeeh Team**
335
+
336
+ ${introEN}
337
+
338
+ ## What's New? šŸš€
339
+
340
+ ${featureListEN}
341
+
342
+ ## How to Upgrade
343
+
344
+ For new users, simply run:
345
+
346
+ \`\`\`bash
347
+ npx lapeeh init my-project
348
+ \`\`\`
349
+
350
+ For existing users who want to update to the latest version:
351
+
352
+ \`\`\`bash
353
+ npm install lapeeh@latest
354
+ \`\`\`
355
+
356
+ Thank you for being part of the lapeeh Framework journey!
357
+ `;
358
+
359
+ fs.writeFileSync(path.join(websiteDir, 'blog', blogFileName), idContent);
360
+ fs.writeFileSync(path.join(websiteDir, 'en/blog', blogFileName), enContent);
361
+
362
+ console.log('šŸ“‘ Updating blog indexes...');
363
+ updateBlogIndex(path.join(websiteDir, 'blog/index.md'), newVersion, titleID, dateString, descriptionID, blogFileName, 'id');
364
+ updateBlogIndex(path.join(websiteDir, 'en/blog/index.md'), newVersion, titleEN, dateStringEn, descriptionEN, blogFileName, 'en');
365
+ } else {
366
+ console.log('ā­ļø Skipping blog generation.');
367
+ }
368
+
369
+ // 3. Question: Documentation
370
+ const updateDocs = await question('\n2. Apa Anda ingin update dokumentasi? (y/n): ');
371
+ if (updateDocs.toLowerCase() === 'y') {
372
+ console.log('\nšŸ“š Documentation Update:');
373
+ console.log('Sistem akan menjalankan sinkronisasi otomatis:');
374
+ console.log(' - Menyalin file dari `doc/id` ke `website/docs`');
375
+ console.log(' - Menyalin file dari `doc/en` ke `website/en/docs`');
376
+ console.log(' - Mengubah nama file menjadi format URL-friendly (contoh: GETTING_STARTED.md -> getting-started.md)');
377
+
378
+ console.log('\nāš ļø Manual Action Required (If applicable):');
379
+ console.log('Jika ada package/method baru, silakan update file berikut secara manual sekarang:');
380
+ console.log(' - website/docs/packages.md');
381
+ console.log(' - website/docs/api.md');
382
+
383
+ await question('Tekan Enter untuk menjalankan sinkronisasi otomatis (setelah Anda selesai update manual)...');
384
+
385
+ console.log('šŸ”„ Syncing documentation...');
386
+ execSync('node scripts/sync-docs.js', { cwd: websiteDir, stdio: 'inherit' });
387
+ } else {
388
+ console.log('ā­ļø Skipping documentation sync.');
389
+ }
390
+
391
+ // 4. Question: Git
392
+ const pushGit = await question('\n3. Apa ingin publish ke Git? (y/n): ');
393
+ if (pushGit.toLowerCase() === 'y') {
394
+ const commitMsg = blogTitleEN
395
+ ? `chore: release v${newVersion} - ${blogTitleEN}`
396
+ : `chore: release v${newVersion}`;
397
+
398
+ try {
399
+ execSync('git add .', { stdio: 'inherit' });
400
+ execSync(`git commit -m "${commitMsg}"`, { stdio: 'inherit' });
401
+ } catch (e) {
402
+ console.log('āš ļø No changes to commit or commit failed. Continuing...');
403
+ }
404
+
405
+ try {
406
+ // Delete tag if it exists locally to avoid "already exists" error
407
+ execSync(`git tag -d v${newVersion}`, { stdio: 'ignore' });
408
+ } catch (e) {
409
+ // Ignore if tag doesn't exist
410
+ }
411
+
412
+ execSync(`git tag v${newVersion}`, { stdio: 'inherit' });
413
+ execSync(`git push origin HEAD`, { stdio: 'inherit' });
414
+
415
+ try {
416
+ execSync(`git push origin v${newVersion}`, { stdio: 'inherit' });
417
+ } catch (e) {
418
+ console.log('āš ļø Tag push failed. Trying force push (updating existing tag)...');
419
+ execSync(`git push origin v${newVersion} --force`, { stdio: 'inherit' });
420
+ }
421
+
422
+ console.log('āœ… Git push & tag complete');
423
+ } else {
424
+ console.log('ā­ļø Skipping Git push.');
425
+ }
426
+
427
+ // 5. Question: NPM
428
+ const publishNpm = await question('\n4. Apa ingin publish ke NPM? (y/n): ');
429
+ if (publishNpm.toLowerCase() === 'y') {
430
+ try {
431
+ execSync('npm publish', { stdio: 'inherit' });
432
+ console.log('āœ… NPM publish complete');
433
+ } catch (error) {
434
+ console.log('\nāš ļø NPM Publish failed. This might be due to 2FA.');
435
+ const otp = await question('šŸ” Masukkan kode OTP (Authenticator App) Anda: ');
436
+ if (otp && otp.trim() !== '') {
437
+ execSync(`npm publish --otp=${otp.trim()}`, { stdio: 'inherit' });
438
+ console.log('āœ… NPM publish complete');
439
+ } else {
440
+ console.log('āŒ NPM publish aborted.');
441
+ throw error;
442
+ }
443
+ }
444
+ } else {
445
+ console.log('ā­ļø Skipping NPM publish.');
446
+ }
447
+
448
+ console.log('\n✨ Proses selesai!');
449
+
450
+ } catch (error) {
451
+ console.error('āŒ Error:', error.message);
452
+ } finally {
453
+ rl.close();
454
+ }
455
+ }
456
+
457
+ function updatePackageJson(filePath, version) {
458
+ const json = JSON.parse(fs.readFileSync(filePath, 'utf8'));
459
+ json.version = version;
460
+ fs.writeFileSync(filePath, JSON.stringify(json, null, 2) + '\n');
461
+ console.log(`Updated ${path.basename(filePath)} to ${version}`);
462
+ }
463
+
464
+ function updateBlogIndex(filePath, version, title, date, description, fileName, lang) {
465
+ let content = fs.readFileSync(filePath, 'utf8');
466
+
467
+ const readMore = lang === 'id' ? 'Baca selengkapnya' : 'Read more';
468
+ const releaseTag = lang === 'id' ? 'Rilis' : 'Release';
469
+
470
+ // Construct new entry
471
+ const newEntry = `## šŸš€ [${releaseTag} v${version}: ${title}](./${fileName.replace('.md', '')})
472
+
473
+ _${date}_ • šŸ‘¤ lapeeh Team • šŸ·ļø _Release_
474
+
475
+ ${description} [${readMore} →](./${fileName.replace('.md', '')})
476
+
477
+ ---
478
+
479
+ `;
480
+
481
+ const separator = '---';
482
+ const parts = content.split(separator);
483
+
484
+ if (parts.length >= 2) {
485
+ parts.splice(1, 0, '\n\n' + newEntry.trim() + '\n\n');
486
+ content = parts.join(separator);
487
+ } else {
488
+ content = content + '\n\n' + newEntry;
489
+ }
490
+
491
+ fs.writeFileSync(filePath, content);
492
+ }
493
+
494
+ main();