@skalfa/skalfa-cli 1.0.12 → 1.0.13

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.
@@ -63,22 +63,25 @@ program
63
63
  .command("init")
64
64
  .description("Initialize a new Skalfa monorepo project containing both API and App.")
65
65
  .argument("[name]", "project folder name")
66
- .action(async (name) => {
67
- await runCommand(() => (0, init_1.initProject)(name));
66
+ .option("-a, --auth <type>", "Authentication type: username or email")
67
+ .action(async (name, options) => {
68
+ await runCommand(() => (0, init_1.initProject)(name, options?.auth ? { authType: options.auth } : undefined));
68
69
  });
69
70
  program
70
71
  .command("create:api")
71
72
  .description("Create a new Skalfa API project.")
72
73
  .argument("<name>", "project folder and package name")
73
- .action(async (name) => {
74
- await runCommand(() => (0, create_api_1.createApi)(name));
74
+ .option("-a, --auth <type>", "Authentication type: username or email")
75
+ .action(async (name, options) => {
76
+ await runCommand(() => (0, create_api_1.createApi)(name, options.auth ? { authType: options.auth } : undefined));
75
77
  });
76
78
  program
77
79
  .command("create:app")
78
80
  .description("Create a new Skalfa App Next.js project.")
79
81
  .argument("<name>", "project folder and package name")
80
- .action(async (name) => {
81
- await runCommand(() => (0, create_app_1.createApp)(name));
82
+ .option("-a, --auth <type>", "Authentication type: username or email")
83
+ .action(async (name, options) => {
84
+ await runCommand(() => (0, create_app_1.createApp)(name, options.auth ? { authType: options.auth } : undefined));
82
85
  });
83
86
  program
84
87
  .command("add")
@@ -67,6 +67,33 @@ async function installAgent(overrideType, targetProjectDir = process.cwd()) {
67
67
  console.log(`Created initial record: ${file}`);
68
68
  }
69
69
  }
70
+ if (type === "app") {
71
+ // Detect auth type from page.tsx or similar
72
+ const loginPagePath = node_path_1.default.join(targetProjectDir, "app", "auth", "login", "page.tsx");
73
+ let isEmail = true;
74
+ if (node_fs_1.default.existsSync(loginPagePath)) {
75
+ const loginContent = node_fs_1.default.readFileSync(loginPagePath, "utf8");
76
+ isEmail = loginContent.includes("email");
77
+ }
78
+ const updateFeaturesFile = (filePath) => {
79
+ if (node_fs_1.default.existsSync(filePath)) {
80
+ let content = node_fs_1.default.readFileSync(filePath, "utf8");
81
+ if (isEmail) {
82
+ content = content
83
+ .replace(/\|\s*`admin`\s*\|/g, "| `admin@mail.com` |")
84
+ .replace(/\|\s*`user`\s*\|/g, "| `user@mail.com` |");
85
+ }
86
+ else {
87
+ content = content
88
+ .replace(/\|\s*`admin`\s*\|/g, "| `admin` |")
89
+ .replace(/\|\s*`user`\s*\|/g, "| `user` |");
90
+ }
91
+ node_fs_1.default.writeFileSync(filePath, content, "utf8");
92
+ }
93
+ };
94
+ updateFeaturesFile(node_path_1.default.join(templatesDir, "features.md"));
95
+ updateFeaturesFile(node_path_1.default.join(recordsDir, "features.md"));
96
+ }
70
97
  }
71
98
  // 3. Add /.agents/ to project's .gitignore
72
99
  const gitignorePath = node_path_1.default.join(targetProjectDir, ".gitignore");
@@ -48,10 +48,13 @@ async function createApi(projectName, options) {
48
48
  let hasCron = options?.cron ?? false;
49
49
  let hasDa = options?.da ?? false;
50
50
  let hasSocket = options?.socket ?? false;
51
+ let authType = options?.authType ?? "username";
51
52
  if (!options) {
52
53
  // Ask interactive questions sequentially using a single readline interface
53
54
  const q = new Questioner();
54
55
  try {
56
+ const authChoice = (await q.ask("Choose authentication type (username/email) [default: username]: ", "username")).toLowerCase();
57
+ authType = authChoice === "email" ? "email" : "username";
55
58
  hasRedis = (await q.ask("Do you need Redis? (y/N): ", "No")).toLowerCase().startsWith("y");
56
59
  hasQueue = (await q.ask("Do you need Queue? (y/N): ", "No")).toLowerCase().startsWith("y");
57
60
  hasCache = (await q.ask("Do you need Cache? (y/N): ", "No")).toLowerCase().startsWith("y");
@@ -188,7 +191,8 @@ If you are an AI coding agent assisting with this project, please make sure to r
188
191
  cache: hasCache,
189
192
  cron: hasCron,
190
193
  da: hasDa,
191
- socket: hasSocket
194
+ socket: hasSocket,
195
+ authType: authType
192
196
  });
193
197
  spinner.update("Installing dependencies (this may take a moment)...");
194
198
  await (0, installer_1.installDependenciesAsync)(target);
@@ -221,35 +225,37 @@ function customizeProject(target, opts) {
221
225
  const pkg = JSON.parse(node_fs_1.default.readFileSync(packageJsonPath, "utf8"));
222
226
  pkg.dependencies = pkg.dependencies || {};
223
227
  pkg.scripts = pkg.scripts || {};
228
+ const isMonorepo = node_path_1.default.basename(target) === "api" || node_path_1.default.basename(target) === "app";
229
+ const devPathPrefix = isMonorepo ? "file:../../" : "file:../";
224
230
  // Base ORM integration (always included)
225
- pkg.dependencies["@skalfa/skalfa-orm"] = isDev ? "file:../skalfa-orm" : "^1.0.0";
231
+ pkg.dependencies["@skalfa/skalfa-orm"] = isDev ? `${devPathPrefix}skalfa-orm` : "^1.0.0";
226
232
  if (isDev) {
227
- pkg.dependencies["@skalfa/skalfa-api-core"] = "file:../skalfa-api-core";
233
+ pkg.dependencies["@skalfa/skalfa-api-core"] = `${devPathPrefix}skalfa-api-core`;
228
234
  }
229
235
  const devCommands = ["bun run --watch app/app.ts", "bun skalfa watch:barrels"];
230
236
  if (opts.redis) {
231
- pkg.dependencies["@skalfa/skalfa-redis"] = isDev ? "file:../skalfa-redis" : "^1.0.0";
237
+ pkg.dependencies["@skalfa/skalfa-redis"] = isDev ? `${devPathPrefix}skalfa-redis` : "^1.0.0";
232
238
  pkg.dependencies["ioredis"] = "^5.4.1";
233
239
  }
234
240
  if (opts.queue) {
235
- pkg.dependencies["@skalfa/skalfa-queue"] = isDev ? "file:../skalfa-queue" : "^1.0.0";
241
+ pkg.dependencies["@skalfa/skalfa-queue"] = isDev ? `${devPathPrefix}skalfa-queue` : "^1.0.0";
236
242
  pkg.scripts["start:queue"] = "bun run app/jobs/queues/worker.queue.ts";
237
243
  devCommands.push("bun start:queue");
238
244
  }
239
245
  if (opts.cache) {
240
- pkg.dependencies["@skalfa/skalfa-cache"] = isDev ? "file:../skalfa-cache" : "^1.0.0";
246
+ pkg.dependencies["@skalfa/skalfa-cache"] = isDev ? `${devPathPrefix}skalfa-cache` : "^1.0.0";
241
247
  }
242
248
  if (opts.cron) {
243
- pkg.dependencies["@skalfa/skalfa-cron"] = isDev ? "file:../skalfa-cron" : "^1.0.0";
249
+ pkg.dependencies["@skalfa/skalfa-cron"] = isDev ? `${devPathPrefix}skalfa-cron` : "^1.0.0";
244
250
  pkg.scripts["start:cron"] = "bun run app/jobs/crons/worker.cron.ts";
245
251
  devCommands.push("bun start:cron");
246
252
  }
247
253
  if (opts.da) {
248
- pkg.dependencies["@skalfa/skalfa-da"] = isDev ? "file:../skalfa-da" : "^1.0.0";
254
+ pkg.dependencies["@skalfa/skalfa-da"] = isDev ? `${devPathPrefix}skalfa-da` : "^1.0.0";
249
255
  pkg.dependencies["@clickhouse/client"] = "^1.6.0";
250
256
  }
251
257
  if (opts.socket) {
252
- pkg.dependencies["@skalfa/skalfa-socket"] = isDev ? "file:../skalfa-socket" : "^1.0.0";
258
+ pkg.dependencies["@skalfa/skalfa-socket"] = isDev ? `${devPathPrefix}skalfa-socket` : "^1.0.0";
253
259
  pkg.dependencies["socket.io"] = "^4.7.5";
254
260
  pkg.scripts["start:socket"] = "bun run app/jobs/sockets/worker.socket.ts";
255
261
  devCommands.push("bun start:socket");
@@ -349,6 +355,98 @@ function customizeProject(target, opts) {
349
355
  }
350
356
  node_fs_1.default.writeFileSync(appTsPath, content, "utf8");
351
357
  }
358
+ // 6. Handle authentication type customization
359
+ if (opts.authType === "username") {
360
+ // A. Modify migration: database/migrations/0000_00/users.ts
361
+ const migrationDir = node_path_1.default.join(target, "database", "migrations", "0000_00");
362
+ const migrationUsersPath = node_path_1.default.join(migrationDir, "users.ts");
363
+ if (node_fs_1.default.existsSync(migrationUsersPath)) {
364
+ let content = node_fs_1.default.readFileSync(migrationUsersPath, "utf8");
365
+ // Replace table.string("email").unique().notNullable() with table.string("username").unique().notNullable()
366
+ content = content.replace(/table\.string\("email"\)\.unique\(\)\.notNullable\(\)/g, 'table.string("username").unique().notNullable()');
367
+ // Remove table.timestamp("email_verification_at")
368
+ content = content.replace(/table\.timestamp\("email_verification_at"\)\r?\n?/g, '');
369
+ // Delete the user_mail_tokens table schema creation
370
+ content = content.replace(/await\s+knex\.schema\.createTable\("user_mail_tokens",\s*\((table|t)\)\s*=>\s*\{[\s\S]*?\}\)\r?\n?/g, '');
371
+ node_fs_1.default.writeFileSync(migrationUsersPath, content, "utf8");
372
+ }
373
+ // B. Modify model: app/models/iam/user.model.ts
374
+ const userModelPath = node_path_1.default.join(target, "app", "models", "iam", "user.model.ts");
375
+ if (node_fs_1.default.existsSync(userModelPath)) {
376
+ let content = node_fs_1.default.readFileSync(userModelPath, "utf8");
377
+ // Replace email field with username field
378
+ content = content.replace(/@Field\(\s*\[\s*"fillable"\s*,\s*"selectable"\s*,\s*"searchable"\s*\]\s*\)\s*\r?\n?\s*email!:\s*string/g, '@Field(["fillable", "selectable", "searchable"])\n username!: string');
379
+ // Remove email_verification_at field
380
+ content = content.replace(/@Field\(\s*\[\s*"fillable"\s*,\s*"selectable"\s*,\s*"searchable"\s*\]\s*\)\s*\r?\n?\s*email_verification_at!:\s*Date/g, '');
381
+ node_fs_1.default.writeFileSync(userModelPath, content, "utf8");
382
+ }
383
+ // C. Modify controller: app/controllers/iam/auth.controller.ts
384
+ const authControllerPath = node_path_1.default.join(target, "app", "controllers", "iam", "auth.controller.ts");
385
+ if (node_fs_1.default.existsSync(authControllerPath)) {
386
+ let content = node_fs_1.default.readFileSync(authControllerPath, "utf8");
387
+ // Remove import { UserMailToken } from "app/outputs/mails";
388
+ content = content.replace(/import\s*\{\s*UserMailToken\s*\}\s*from\s*["']app\/outputs\/mails["'];?\r?\n?/g, '');
389
+ // Update validation and logic in login
390
+ content = content.replace(/email\s*:\s*["']required["'],/g, 'username : "required",');
391
+ content = content.replace(/const\s*\{\s*email\s*,\s*password\s*\}\s*=\s*c\.body/g, 'const { username, password } = c.body');
392
+ content = content.replace(/const user\s*=\s*await\s*User\.query\(\)\.where\("email",\s*email\)\.whereNotNull\("email_verification_at"\)\.first\(\);/g, 'const user = await User.query().where("username", username).first();');
393
+ content = content.replace(/if\s*\(!user\)\s*return\s*c\.responseErrorValidation\(\{\s*email\s*:\s*\[\s*["']E-mail not found!["']\s*\]\s*\}\)/g, 'if (!user) return c.responseErrorValidation({username: ["Username not found!"]})');
394
+ // Remove register and verify methods
395
+ content = content.replace(/\/\/\s*=+\s*>\s*\/\/\s*## Register new account\.[\s\S]*?(?=\/\/\s*=+\s*>\s*\/\/\s*## Get logged account)/g, '');
396
+ // Replace update method validation: email: "required" -> username: "required"
397
+ content = content.replace(/email\s*:\s*["']required["'],/g, 'username : "required",');
398
+ node_fs_1.default.writeFileSync(authControllerPath, content, "utf8");
399
+ }
400
+ // D. Modify router: app/routes/base.routes.ts
401
+ const baseRoutesPath = node_path_1.default.join(target, "app", "routes", "base.routes.ts");
402
+ if (node_fs_1.default.existsSync(baseRoutesPath)) {
403
+ let content = node_fs_1.default.readFileSync(baseRoutesPath, "utf8");
404
+ // Comment out register and verify routes
405
+ content = content.replace(/route\.post\('\/register',\s*AuthController\.register\)/g, '// route.post(\'/register\', AuthController.register) - disabled in username auth');
406
+ content = content.replace(/route\.post\('\/verify',\s*AuthController\.verify\)/g, '// route.post(\'/verify\', AuthController.verify) - disabled in username auth');
407
+ node_fs_1.default.writeFileSync(baseRoutesPath, content, "utf8");
408
+ }
409
+ // E. Modify controller: app/controllers/iam/user.controller.ts
410
+ const userControllerPath = node_path_1.default.join(target, "app", "controllers", "iam", "user.controller.ts");
411
+ if (node_fs_1.default.existsSync(userControllerPath)) {
412
+ let content = node_fs_1.default.readFileSync(userControllerPath, "utf8");
413
+ // Replace validation in store: email : ["required", "email"], -> username : ["required"],
414
+ content = content.replace(/email\s*:\s*\[\s*["']required["']\s*,\s*["']email["']\s*\]/g, 'username : ["required"]');
415
+ // Replace validation in update: email : "required", -> username : "required",
416
+ content = content.replace(/email\s*:\s*["']required["']/g, 'username : "required"');
417
+ node_fs_1.default.writeFileSync(userControllerPath, content, "utf8");
418
+ }
419
+ // F. Modify seeder: database/seeders/user.seeder.ts
420
+ const userSeederPath = node_path_1.default.join(target, "database", "seeders", "user.seeder.ts");
421
+ if (node_fs_1.default.existsSync(userSeederPath)) {
422
+ let content = node_fs_1.default.readFileSync(userSeederPath, "utf8");
423
+ // Seed roles: Admin and User
424
+ content = content.replace(/\{"name": "Petugas"\}/g, '{"name": "User"}');
425
+ // Seed users with username instead of email
426
+ content = content.replace(/\{"name": "Admin", "email": "admin@skalfa.id",/g, '{"name": "Admin", "username": "admin",');
427
+ content = content.replace(/\{"name": "Petugas", "email": "petugas@skalfa.id",/g, '{"name": "User", "username": "user",');
428
+ node_fs_1.default.writeFileSync(userSeederPath, content, "utf8");
429
+ }
430
+ // G. Delete app/outputs/mails folder
431
+ const mailsDir = node_path_1.default.join(target, "app", "outputs", "mails");
432
+ if (node_fs_1.default.existsSync(mailsDir)) {
433
+ node_fs_1.default.rmSync(mailsDir, { recursive: true, force: true });
434
+ }
435
+ }
436
+ else {
437
+ // If authType === "email":
438
+ // Change seeder from petugas@skalfa.id to user@mail.com and admin@skalfa.id to admin@mail.com, role Petugas to User
439
+ const userSeederPath = node_path_1.default.join(target, "database", "seeders", "user.seeder.ts");
440
+ if (node_fs_1.default.existsSync(userSeederPath)) {
441
+ let content = node_fs_1.default.readFileSync(userSeederPath, "utf8");
442
+ content = content.replace(/\{"name": "Petugas"\}/g, '{"name": "User"}');
443
+ content = content.replace(/admin@skalfa.id/g, 'admin@mail.com');
444
+ content = content.replace(/\{"name": "Petugas", "email": "petugas@mail.com",/g, '{"name": "User", "email": "user@mail.com",');
445
+ content = content.replace(/\{"name": "Petugas", "email": "petugas@skalfa.id",/g, '{"name": "User", "email": "user@mail.com",');
446
+ content = content.replace(/petugas@skalfa.id/g, 'user@mail.com');
447
+ node_fs_1.default.writeFileSync(userSeederPath, content, "utf8");
448
+ }
449
+ }
352
450
  }
353
451
  function addTsconfigPath(tsconfigPath, packageName) {
354
452
  if (!node_fs_1.default.existsSync(tsconfigPath))
@@ -47,10 +47,13 @@ async function createApp(projectName, options) {
47
47
  let hasPwa = options?.pwa ?? false;
48
48
  let hasTauriDesktop = options?.tauriDesktop ?? false;
49
49
  let hasTauriMobile = options?.tauriMobile ?? false;
50
+ let authType = options?.authType ?? "username";
50
51
  if (!options) {
51
52
  // Ask interactive questions sequentially
52
53
  const q = new Questioner();
53
54
  try {
55
+ const authChoice = (await q.ask("Choose authentication type (username/email) [default: username]: ", "username")).toLowerCase();
56
+ authType = authChoice === "email" ? "email" : "username";
54
57
  hasIdb = (await q.ask("Do you need IndexedDB (IDB)? (y/N): ", "No")).toLowerCase().startsWith("y");
55
58
  hasSocket = (await q.ask("Do you need Socket Client? (y/N): ", "No")).toLowerCase().startsWith("y");
56
59
  hasDocument = (await q.ask("Do you need Document Export/Viewer (PDF/Excel)? (y/N): ", "No")).toLowerCase().startsWith("y");
@@ -174,7 +177,8 @@ If you are an AI coding agent assisting with this project, please make sure to r
174
177
  document: hasDocument,
175
178
  pwa: hasPwa,
176
179
  tauriDesktop: hasTauriDesktop,
177
- tauriMobile: hasTauriMobile
180
+ tauriMobile: hasTauriMobile,
181
+ authType: authType
178
182
  });
179
183
  spinner.update("Installing dependencies (this may take a moment)...");
180
184
  await (0, installer_1.installDependenciesAsync)(target);
@@ -200,6 +204,8 @@ function customizeProject(target, opts) {
200
204
  const packageJsonPath = node_path_1.default.join(target, "package.json");
201
205
  const baseComponentsIndexPath = node_path_1.default.join(target, "components", "base.components", "index.ts");
202
206
  const isDev = !!process.env[TEMPLATE_ENV_KEY];
207
+ const isMonorepo = node_path_1.default.basename(target) === "api" || node_path_1.default.basename(target) === "app";
208
+ const devPathPrefix = isMonorepo ? "file:../../" : "file:../";
203
209
  // 1. Update dependencies and scripts in package.json
204
210
  if (node_fs_1.default.existsSync(packageJsonPath)) {
205
211
  const pkg = JSON.parse(node_fs_1.default.readFileSync(packageJsonPath, "utf8"));
@@ -207,10 +213,13 @@ function customizeProject(target, opts) {
207
213
  pkg.devDependencies = pkg.devDependencies || {};
208
214
  pkg.scripts = pkg.scripts || {};
209
215
  // Core dependency
210
- pkg.dependencies["@skalfa/skalfa-app-core"] = isDev ? "file:../skalfa-app-core" : "^1.0.0";
216
+ pkg.dependencies["@skalfa/skalfa-app-core"] = isDev ? `${devPathPrefix}skalfa-app-core` : "^1.0.0";
217
+ if (isDev && pkg.dependencies["@skalfa/skalfa-component"]) {
218
+ pkg.dependencies["@skalfa/skalfa-component"] = `${devPathPrefix}skalfa-component`;
219
+ }
211
220
  // A. IndexedDB Option
212
221
  if (opts.idb) {
213
- pkg.dependencies["@skalfa/skalfa-idb"] = isDev ? "file:../skalfa-idb" : "^1.0.0";
222
+ pkg.dependencies["@skalfa/skalfa-idb"] = isDev ? `${devPathPrefix}skalfa-idb` : "^1.0.0";
214
223
  }
215
224
  else {
216
225
  // Delete schema directory
@@ -242,12 +251,12 @@ function customizeProject(target, opts) {
242
251
  }
243
252
  // B. Socket Option
244
253
  if (opts.socket) {
245
- pkg.dependencies["@skalfa/skalfa-socket-client"] = isDev ? "file:../skalfa-socket-client" : "^1.0.0";
254
+ pkg.dependencies["@skalfa/skalfa-socket-client"] = isDev ? `${devPathPrefix}skalfa-socket-client` : "^1.0.0";
246
255
  pkg.dependencies["socket.io-client"] = "^4.8.1";
247
256
  }
248
257
  // C. Document Option
249
258
  if (opts.document) {
250
- pkg.dependencies["@skalfa/skalfa-document"] = isDev ? "file:../skalfa-document" : "^1.0.0";
259
+ pkg.dependencies["@skalfa/skalfa-document"] = isDev ? `${devPathPrefix}skalfa-document` : "^1.0.0";
251
260
  pkg.dependencies["exceljs"] = "^4.4.0";
252
261
  pkg.dependencies["pdf-lib"] = "^1.17.1";
253
262
  pkg.dependencies["pdfjs-dist"] = "^4.4.168";
@@ -316,4 +325,54 @@ function customizeProject(target, opts) {
316
325
  }
317
326
  node_fs_1.default.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2), "utf8");
318
327
  }
328
+ // 6. Handle authentication type customization
329
+ if (opts.authType === "username") {
330
+ // A. Modify app/auth/login/page.tsx
331
+ const loginPagePath = node_path_1.default.join(target, "app", "auth", "login", "page.tsx");
332
+ if (node_fs_1.default.existsSync(loginPagePath)) {
333
+ let content = node_fs_1.default.readFileSync(loginPagePath, "utf8");
334
+ // Replace email field with username field
335
+ content = content.replace(/\{\s*construction:\s*\{\s*name:\s*["']email["'],\s*label:\s*["']E-mail["'],\s*placeholder:\s*["']Ex:\s*example@mail\.com["'],\s*validations:\s*["']required\|min:10\|max:50\|email["']\s*\}\s*\}/g, `{\n construction: {\n name: "username",\n label: "Username",\n placeholder: "Ex: joko.gunawan",\n validations: "required|min:3|max:50"\n }\n }`);
336
+ // Remove Create Account link in login page
337
+ content = content.replace(/<p className="mt-4 text-center">Don&apos;t have an account yet\? <Link href="\/auth\/register" className="text-primary underline">Create Account<\/Link><\/p>/g, '');
338
+ node_fs_1.default.writeFileSync(loginPagePath, content, "utf8");
339
+ }
340
+ // B. Modify app/auth/edit/page.tsx
341
+ const editPagePath = node_path_1.default.join(target, "app", "auth", "edit", "page.tsx");
342
+ if (node_fs_1.default.existsSync(editPagePath)) {
343
+ let content = node_fs_1.default.readFileSync(editPagePath, "utf8");
344
+ // Replace email field with username field
345
+ content = content.replace(/\{\s*construction:\s*\{\s*name:\s*["']email["'],\s*label:\s*["']E-mail["'],\s*placeholder:\s*["']Ex:\s*example@mail\.com["'],\s*\}\s*\}/g, `{\n construction: {\n name: "username",\n label: "Username",\n placeholder: "Ex: joko.gunawan",\n }\n }`);
346
+ node_fs_1.default.writeFileSync(editPagePath, content, "utf8");
347
+ }
348
+ // C. Modify app/auth/me/page.tsx
349
+ const mePagePath = node_path_1.default.join(target, "app", "auth", "me", "page.tsx");
350
+ if (node_fs_1.default.existsSync(mePagePath)) {
351
+ let content = node_fs_1.default.readFileSync(mePagePath, "utf8");
352
+ // Replace email display with username
353
+ content = content.replace(/<div>\s*<p className="text-xs font-semibold text-light-foreground">\s*Email\s*<\/p>\s*<p>\{user\?\.email\}<\/p>\s*<\/div>/g, `<div>\n <p className="text-xs font-semibold text-light-foreground">\n Username\n </p>\n <p>{user?.username}</p>\n </div>`);
354
+ node_fs_1.default.writeFileSync(mePagePath, content, "utf8");
355
+ }
356
+ // D. Modify app/dashboard/user/page.tsx
357
+ const userPagePath = node_path_1.default.join(target, "app", "dashboard", "user", "page.tsx");
358
+ if (node_fs_1.default.existsSync(userPagePath)) {
359
+ let content = node_fs_1.default.readFileSync(userPagePath, "utf8");
360
+ // Replace table column for email
361
+ content = content.replace(/\{\s*selector:\s*["']email["'],\s*label:\s*["']Email["'],\s*sortable:\s*true,\s*width:\s*["']250px["'],\s*\}/g, `{\n selector: "username",\n label: "Username",\n sortable: true,\n width: "250px",\n }`);
362
+ // Replace detail panel for email
363
+ content = content.replace(/\{\s*label:\s*["']Email["'],\s*item:\s*["']email["'],\s*\}/g, `{\n label: "Username",\n item: "username",\n }`);
364
+ // Replace form field for email
365
+ content = content.replace(/\{\s*construction:\s*\{\s*name:\s*["']email["'],\s*label:\s*["']E-mail["'],\s*placeholder:\s*["']Ex:\s*example@mail\.com["'],\s*validations:\s*\[\s*["']required["']\s*\]\s*,\s*\}\s*,?\s*\}/g, `{\n construction: {\n name: "username",\n label: "Username",\n placeholder: "Ex: joko.gunawan",\n validations: ["required"],\n },\n }`);
366
+ node_fs_1.default.writeFileSync(userPagePath, content, "utf8");
367
+ }
368
+ // E. Delete app/auth/register and app/auth/verify folders (self-registration and verification disabled/removed)
369
+ const registerDir = node_path_1.default.join(target, "app", "auth", "register");
370
+ if (node_fs_1.default.existsSync(registerDir)) {
371
+ node_fs_1.default.rmSync(registerDir, { recursive: true, force: true });
372
+ }
373
+ const verifyDir = node_path_1.default.join(target, "app", "auth", "verify");
374
+ if (node_fs_1.default.existsSync(verifyDir)) {
375
+ node_fs_1.default.rmSync(verifyDir, { recursive: true, force: true });
376
+ }
377
+ }
319
378
  }
@@ -30,7 +30,7 @@ class Questioner {
30
30
  this.rl.close();
31
31
  }
32
32
  }
33
- async function initProject(projectName) {
33
+ async function initProject(projectName, options) {
34
34
  const cwd = process.cwd();
35
35
  const isCurrentDir = !projectName || projectName === ".";
36
36
  const target = isCurrentDir ? cwd : node_path_1.default.resolve(cwd, projectName);
@@ -62,7 +62,14 @@ async function initProject(projectName) {
62
62
  let appPwa = false;
63
63
  let appTauriDesktop = false;
64
64
  let appTauriMobile = false;
65
+ let authType = options?.authType ?? "username";
66
+ const shouldPromptAuth = !options || !options.authType;
65
67
  try {
68
+ if (shouldPromptAuth) {
69
+ console.log("\n--- Configure Authentication ---");
70
+ const authChoice = (await q.ask("Choose authentication type (username/email) [default: username]: ", "username")).toLowerCase();
71
+ authType = authChoice === "email" ? "email" : "username";
72
+ }
66
73
  console.log("\n--- Configure API (Backend) ---");
67
74
  apiRedis = (await q.ask("Do you need Redis? (y/N): ", "No")).toLowerCase().startsWith("y");
68
75
  apiQueue = (await q.ask("Do you need Queue? (y/N): ", "No")).toLowerCase().startsWith("y");
@@ -93,6 +100,7 @@ async function initProject(projectName) {
93
100
  cron: apiCron,
94
101
  da: apiDa,
95
102
  socket: apiSocket,
103
+ authType: authType,
96
104
  });
97
105
  console.log("\nCreating App...");
98
106
  await (0, create_app_1.createApp)(appDir, {
@@ -102,6 +110,7 @@ async function initProject(projectName) {
102
110
  pwa: appPwa,
103
111
  tauriDesktop: appTauriDesktop,
104
112
  tauriMobile: appTauriMobile,
113
+ authType: authType,
105
114
  });
106
115
  console.log("\nInstalling AI Agents...");
107
116
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skalfa/skalfa-cli",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "description": "Command Line Interface tool for scaffolding Skalfa projects, managing extensions, and ejecting core utilities.",
5
5
  "main": "dist/bin/skalfa.js",
6
6
  "bin": {