@idevconn/create-icore 0.4.1 → 0.5.1

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/dist/cli.js +304 -24
  2. package/dist/index.cjs +301 -23
  3. package/dist/index.d.cts +7 -1
  4. package/dist/index.d.ts +7 -1
  5. package/dist/index.js +300 -23
  6. package/package.json +1 -1
  7. package/templates/apps/api/.env.example +14 -0
  8. package/templates/apps/api/src/app/app.module.ts +5 -1
  9. package/templates/apps/api/src/main.ts +12 -6
  10. package/templates/apps/microservices/auth/package.json +1 -1
  11. package/templates/apps/microservices/auth/project.json +2 -1
  12. package/templates/apps/microservices/auth/src/app/app.module.ts +50 -39
  13. package/templates/apps/microservices/auth/src/main.ts +6 -23
  14. package/templates/apps/microservices/jobs/project.json +2 -1
  15. package/templates/apps/microservices/jobs/src/app/redis-connection.ts +35 -0
  16. package/templates/apps/microservices/jobs/src/app/workers/cleanup.worker.ts +2 -1
  17. package/templates/apps/microservices/jobs/src/app/workers/email.worker.ts +2 -1
  18. package/templates/apps/microservices/jobs/src/app/workers/image-process.worker.ts +2 -1
  19. package/templates/apps/microservices/notes/project.json +2 -1
  20. package/templates/apps/microservices/notes/src/app/app.module.ts +52 -38
  21. package/templates/apps/microservices/notes/src/main.ts +6 -23
  22. package/templates/apps/microservices/payment/project.json +2 -1
  23. package/templates/apps/microservices/payment/src/app/app.module.ts +37 -12
  24. package/templates/apps/microservices/payment/src/main.ts +6 -23
  25. package/templates/apps/microservices/upload/package.json +1 -1
  26. package/templates/apps/microservices/upload/project.json +2 -1
  27. package/templates/apps/microservices/upload/src/app/app.module.ts +50 -42
  28. package/templates/apps/microservices/upload/src/main.ts +6 -23
  29. package/templates/apps/templates/client-antd/.env.example +7 -0
  30. package/templates/apps/templates/client-antd/vite.config.mts +4 -4
  31. package/templates/apps/templates/client-mui/.env.example +7 -0
  32. package/templates/apps/templates/client-mui/vite.config.mts +4 -4
  33. package/templates/apps/templates/client-shadcn/.env.example +6 -1
  34. package/templates/apps/templates/client-shadcn/vite.config.mts +4 -4
  35. package/templates/libs/auth-client/src/index.ts +1 -0
  36. package/templates/libs/auth-client/src/lib/auth-client.module.ts +1 -1
  37. package/templates/libs/auth-client/src/lib/auth-client.service.ts +1 -1
  38. package/templates/libs/auth-client/src/lib/auth-client.tokens.ts +4 -0
  39. package/templates/libs/firebase-admin/README.md +11 -0
  40. package/templates/libs/firebase-admin/eslint.config.mjs +24 -0
  41. package/templates/libs/firebase-admin/package.json +12 -0
  42. package/templates/libs/firebase-admin/project.json +19 -0
  43. package/templates/libs/firebase-admin/src/index.ts +1 -0
  44. package/templates/libs/firebase-admin/src/lib/__tests__/firebase-admin.unit.test.ts +105 -0
  45. package/templates/libs/firebase-admin/src/lib/firebase-admin.ts +70 -0
  46. package/templates/libs/firebase-admin/tsconfig.json +23 -0
  47. package/templates/libs/firebase-admin/tsconfig.lib.json +23 -0
  48. package/templates/libs/firebase-admin/tsconfig.spec.json +22 -0
  49. package/templates/libs/firebase-admin/vitest.config.mts +21 -0
  50. package/templates/libs/jobs-client/src/index.ts +1 -0
  51. package/templates/libs/jobs-client/src/lib/jobs-client.module.ts +1 -1
  52. package/templates/libs/jobs-client/src/lib/jobs-client.service.ts +15 -3
  53. package/templates/libs/jobs-client/src/lib/jobs-client.tokens.ts +4 -0
  54. package/templates/libs/notes-client/src/index.ts +1 -0
  55. package/templates/libs/notes-client/src/lib/notes-client.module.ts +1 -1
  56. package/templates/libs/notes-client/src/lib/notes-client.service.ts +1 -1
  57. package/templates/libs/notes-client/src/lib/notes-client.tokens.ts +4 -0
  58. package/templates/libs/payment-client/src/index.ts +1 -0
  59. package/templates/libs/payment-client/src/lib/payment-client.module.ts +1 -1
  60. package/templates/libs/payment-client/src/lib/payment-client.service.ts +1 -1
  61. package/templates/libs/payment-client/src/lib/payment-client.tokens.ts +4 -0
  62. package/templates/libs/shared/src/__tests__/bootstrap.unit.test.ts +92 -0
  63. package/templates/libs/shared/src/__tests__/transport.unit.test.ts +14 -2
  64. package/templates/libs/shared/src/bootstrap.ts +79 -0
  65. package/templates/libs/shared/src/env.ts +88 -0
  66. package/templates/libs/shared/src/index.ts +2 -0
  67. package/templates/libs/shared/src/transport.ts +62 -3
  68. package/templates/libs/upload-client/src/index.ts +1 -0
  69. package/templates/libs/upload-client/src/lib/upload-client.module.ts +1 -1
  70. package/templates/libs/upload-client/src/lib/upload-client.service.ts +1 -1
  71. package/templates/libs/upload-client/src/lib/upload-client.tokens.ts +4 -0
  72. package/templates/libs/vite-plugins/src/index.d.mts +6 -0
  73. package/templates/libs/vite-plugins/src/index.mjs +50 -0
  74. package/templates/package.json +1 -0
  75. package/templates/tools/create-icore/_template-shell/package.json +1 -0
  76. package/templates/tsconfig.base.json +2 -1
package/dist/cli.js CHANGED
@@ -201,6 +201,12 @@ Re-run with @latest to refresh:
201
201
  });
202
202
  if (p.isCancel(transport)) throw new Error("cancelled");
203
203
  const packageManager = flags.packageManager ?? detectPackageManager();
204
+ if (packageManager === "yarn") {
205
+ p.note(
206
+ "yarn 4.15+ enforces a 24h publish-age gate (npmMinimalAgeGate=1d), so a\n`yarn create @idevconn/icore@latest` run within 24h of a release resolves an\nolder version. If the banner above shows an unexpectedly old version, either:\n \u2022 wait \u2014 the version auto-unlocks 24h after publish, or\n \u2022 bypass once: yarn config set npmMinimalAgeGate 0 (then re-run), or\n \u2022 use npm/pnpm: npm init @idevconn/icore@latest <name> -- [flags]",
207
+ "\u26A0 yarn 24h age-gate"
208
+ );
209
+ }
204
210
  const initGit = flags.initGit ?? !await p.confirm({ message: "Initialise git repo?", initialValue: true }) === false;
205
211
  const install = flags.install ?? !await p.confirm({
206
212
  message: `Run ${packageManager} install?`,
@@ -228,6 +234,13 @@ import { copyFile, mkdir, readdir, readFile as readFile2, stat, writeFile, rm }
228
234
  import { readFileSync } from "fs";
229
235
  import { join as join2 } from "path";
230
236
  import { spawnSync } from "child_process";
237
+
238
+ // src/lib/options.ts
239
+ function pmRun(pm, script) {
240
+ return pm === "npm" ? `npm run ${script}` : `${pm} ${script}`;
241
+ }
242
+
243
+ // src/lib/scaffold.ts
231
244
  var IGNORE_TOP = /* @__PURE__ */ new Set([
232
245
  ".git",
233
246
  "node_modules",
@@ -260,9 +273,29 @@ async function rewriteRootPackageJson(targetDir, opts) {
260
273
  pkg["version"] = "0.0.1";
261
274
  pkg["private"] = true;
262
275
  delete pkg.description;
276
+ if (opts.transport === "nats") {
277
+ const deps = pkg["dependencies"] ??= {};
278
+ deps["nats"] = "^2.29.3";
279
+ }
263
280
  if (opts.packageManager !== "yarn") {
264
281
  delete pkg.packageManager;
265
282
  }
283
+ if (opts.packageManager === "pnpm") {
284
+ pkg["pnpm"] = {
285
+ onlyBuiltDependencies: [
286
+ "@firebase/util",
287
+ "@nestjs/core",
288
+ "@parcel/watcher",
289
+ "@scarf/scarf",
290
+ "@swc/core",
291
+ "less",
292
+ "msgpackr-extract",
293
+ "nx",
294
+ "protobufjs",
295
+ "unrs-resolver"
296
+ ]
297
+ };
298
+ }
266
299
  await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
267
300
  }
268
301
  async function writeAuthEnv(targetDir, opts) {
@@ -300,9 +333,9 @@ async function writeNotesEnv(targetDir, opts) {
300
333
  async function writeGatewayEnv(targetDir, opts) {
301
334
  const envExample = join2(targetDir, "apps/api/.env.example");
302
335
  const env = await readFile2(envExample, "utf8");
303
- let next = env.replace(/^AUTH_TRANSPORT=.*$/m, `AUTH_TRANSPORT=${opts.transport}`).replace(/^UPLOAD_TRANSPORT=.*$/m, `UPLOAD_TRANSPORT=${opts.transport}`);
336
+ let next = env.replace(/^AUTH_TRANSPORT=.*$/m, `AUTH_TRANSPORT=${opts.transport}`).replace(/^UPLOAD_TRANSPORT=.*$/m, `UPLOAD_TRANSPORT=${opts.transport}`).replace(/^NOTES_TRANSPORT=.*$/m, `NOTES_TRANSPORT=${opts.transport}`).replace(/^PAYMENT_TRANSPORT=.*$/m, `PAYMENT_TRANSPORT=${opts.transport}`);
304
337
  if (opts.transport !== "tcp") {
305
- next = next.replace(/^# (AUTH_(?:REDIS|NATS)_URL=)/m, "$1").replace(/^# (UPLOAD_(?:REDIS|NATS)_URL=)/m, "$1");
338
+ next = next.replace(/^# (AUTH_(?:REDIS|NATS)_URL=)/m, "$1").replace(/^# (UPLOAD_(?:REDIS|NATS)_URL=)/m, "$1").replace(/^# (NOTES_(?:REDIS|NATS)_URL=)/m, "$1").replace(/^# (PAYMENT_(?:REDIS|NATS)_URL=)/m, "$1");
306
339
  }
307
340
  await writeFile(join2(targetDir, "apps/api/.env"), next);
308
341
  }
@@ -315,6 +348,25 @@ async function writeRootEnv(targetDir, opts) {
315
348
  ];
316
349
  await writeFile(join2(targetDir, ".env"), lines.join("\n"));
317
350
  }
351
+ async function stripGatewayTransport(targetDir, prefix) {
352
+ const gatewayEnv = join2(targetDir, "apps/api/.env");
353
+ try {
354
+ const env = await readFile2(gatewayEnv, "utf8");
355
+ const next = env.split("\n").filter(
356
+ (line) => !line.startsWith(`${prefix}_`) && !line.startsWith(`# ${prefix}_`) && !line.includes(`${prefix} MS transport`)
357
+ ).join("\n");
358
+ await writeFile(gatewayEnv, next);
359
+ } catch {
360
+ }
361
+ }
362
+ async function writeClientEnv(targetDir) {
363
+ const envExample = join2(targetDir, "apps/client/.env.example");
364
+ try {
365
+ const env = await readFile2(envExample, "utf8");
366
+ await writeFile(join2(targetDir, "apps/client/.env"), env);
367
+ } catch {
368
+ }
369
+ }
318
370
  async function writePaymentEnv(targetDir, opts) {
319
371
  if (opts.payment === "none") return;
320
372
  const envExample = join2(targetDir, "apps/microservices/payment/.env.example");
@@ -391,6 +443,7 @@ async function removePaymentStack(targetDir) {
391
443
  "@icore/payment-client",
392
444
  "@idevconn/payment"
393
445
  ]);
446
+ await stripGatewayTransport(targetDir, "PAYMENT");
394
447
  }
395
448
  async function removeNotesStack(targetDir) {
396
449
  for (const p3 of [
@@ -412,6 +465,7 @@ async function removeNotesStack(targetDir) {
412
465
  } catch {
413
466
  }
414
467
  await stripDeps(join2(targetDir, "apps/api/package.json"), ["@icore/notes-client"]);
468
+ await stripGatewayTransport(targetDir, "NOTES");
415
469
  const tsconfigPath = join2(targetDir, "tsconfig.base.json");
416
470
  try {
417
471
  const src = await readFile2(tsconfigPath, "utf8");
@@ -468,15 +522,17 @@ async function stripTsconfigPath(targetDir, alias) {
468
522
  }
469
523
  async function removeUnusedAuthStrategies(targetDir, authProvider) {
470
524
  const modulePath = join2(targetDir, "apps/microservices/auth/src/app/app.module.ts");
525
+ const AUTH_BRANCH = /if \(provider === 'supabase'\) return makeSupabaseAuth\(cfg\);\n\s*return makeFirebaseAuth\(cfg\);/m;
471
526
  if (authProvider === "supabase") {
472
527
  await rm(join2(targetDir, "libs/auth-strategies/firebase"), { recursive: true, force: true });
473
528
  await stripDeps(join2(targetDir, "apps/microservices/auth/package.json"), [
474
- "@icore/auth-firebase"
529
+ "@icore/auth-firebase",
530
+ "@icore/firebase-admin"
475
531
  ]);
476
532
  await stripTsconfigPath(targetDir, "@icore/auth-firebase");
477
533
  try {
478
534
  const src = await readFile2(modulePath, "utf8");
479
- const next = src.replace(/^import \* as admin from 'firebase-admin';\n/m, "").replace(/^import \{[^}]*FirebaseAuthStrategy[^}]*\} from '@icore\/auth-firebase';\n/m, "").replace(/^function makeFirebaseStrategy\b[\s\S]*?\n^}\n/m, "").replace(/(?<=\n) *case 'firebase':\n *return makeFirebaseStrategy\(cfg\);\n/, "");
535
+ const next = src.replace(/^import \{[^}]*\} from '@icore\/firebase-admin';\n/m, "").replace(/^import \{[^}]*FirebaseAuthStrategy[^}]*\} from '@icore\/auth-firebase';\n/m, "").replace(/^ {2}firebase: \[[^\]]*\],\n/m, "").replace(/\nfunction makeFirebaseAuth[\s\S]*?\n}\n/m, "").replace(AUTH_BRANCH, "return makeSupabaseAuth(cfg);");
480
536
  await writeFile(modulePath, next);
481
537
  } catch {
482
538
  }
@@ -489,10 +545,7 @@ async function removeUnusedAuthStrategies(targetDir, authProvider) {
489
545
  await stripTsconfigPath(targetDir, "@icore/auth-supabase");
490
546
  try {
491
547
  const src = await readFile2(modulePath, "utf8");
492
- const next = src.replace(/^import \{ createClient \} from '@supabase\/supabase-js';\n/m, "").replace(/^import \{[^}]*SupabaseAuthStrategy[^}]*\} from '@icore\/auth-supabase';\n/m, "").replace(
493
- /\n {10}case 'supabase': \{[\s\S]*?return new SupabaseAuthStrategy\(\{ client \}\);\n {10}\}\n/m,
494
- ""
495
- );
548
+ const next = src.replace(/^import \{ createClient \} from '@supabase\/supabase-js';\n/m, "").replace(/^import \{[^}]*SupabaseAuthStrategy[^}]*\} from '@icore\/auth-supabase';\n/m, "").replace(/\nfunction makeSupabaseAuth[\s\S]*?\n}\n/m, "").replace(AUTH_BRANCH, "return makeFirebaseAuth(cfg);");
496
549
  await writeFile(modulePath, next);
497
550
  } catch {
498
551
  }
@@ -504,7 +557,8 @@ async function removeUnusedStorageStrategies(targetDir, uploadProvider) {
504
557
  if (uploadProvider !== "firebase") {
505
558
  await rm(join2(targetDir, "libs/storage-strategies/firebase"), { recursive: true, force: true });
506
559
  await stripDeps(join2(targetDir, "apps/microservices/upload/package.json"), [
507
- "@icore/storage-firebase"
560
+ "@icore/storage-firebase",
561
+ "@icore/firebase-admin"
508
562
  ]);
509
563
  await stripTsconfigPath(targetDir, "@icore/storage-firebase");
510
564
  }
@@ -528,26 +582,26 @@ async function removeUnusedStorageStrategies(targetDir, uploadProvider) {
528
582
  try {
529
583
  let src = await readFile2(modulePath, "utf8");
530
584
  if (uploadProvider !== "firebase") {
531
- src = src.replace(/^import \* as admin from 'firebase-admin';\n/m, "").replace(
585
+ src = src.replace(/^import \{[^}]*\} from '@icore\/firebase-admin';\n/m, "").replace(
532
586
  /^import \{[^}]*FirebaseStorageStrategy[^}]*\} from '@icore\/storage-firebase';\n/m,
533
587
  ""
534
- ).replace(/^function makeFirebaseStorage\b[\s\S]*?\n^}\n/m, "").replace(/(?<=\n) *case 'firebase':\n *return makeFirebaseStorage\(cfg\);\n/, "");
588
+ ).replace(/^ {2}firebase: \[[^\]]*\],\n/m, "").replace(/\nfunction makeFirebaseStorage[\s\S]*?\n}\n/m, "");
535
589
  }
536
590
  if (uploadProvider !== "cloudinary") {
537
591
  src = src.replace(/^import \{ v2 as cloudinary \} from 'cloudinary';\n/m, "").replace(
538
592
  /^import \{[^}]*CloudinaryStorageStrategy[^}]*\} from '@icore\/storage-cloudinary';\n/m,
539
593
  ""
540
- ).replace(/^function makeCloudinaryStorage\b[\s\S]*?\n^}\n/m, "").replace(/(?<=\n) *case 'cloudinary':\n *return makeCloudinaryStorage\(cfg\);\n/, "");
594
+ ).replace(/\nfunction makeCloudinaryStorage[\s\S]*?\n}\n/m, "");
541
595
  }
542
596
  if (uploadProvider !== "supabase") {
543
597
  src = src.replace(/^import \{ createClient \} from '@supabase\/supabase-js';\n/m, "").replace(
544
598
  /^import \{[^}]*SupabaseStorageStrategy[^}]*\} from '@icore\/storage-supabase';\n/m,
545
599
  ""
546
- ).replace(
547
- /\n {10}case 'supabase': \{[\s\S]*?bucket: requireEnv\(cfg, 'SUPABASE_STORAGE_BUCKET'\),\n {12}\}\);\n {10}\}\n/m,
548
- ""
549
- );
600
+ ).replace(/\nfunction makeSupabaseStorage[\s\S]*?\n}\n/m, "");
550
601
  }
602
+ const STORAGE_BRANCH = /if \(provider === 'supabase'\) return makeSupabaseStorage\(cfg\);\n\s*if \(provider === 'firebase'\) return makeFirebaseStorage\(cfg\);\n\s*return makeCloudinaryStorage\(cfg\);/m;
603
+ const chosenReturn = `return make${uploadProvider.charAt(0).toUpperCase() + uploadProvider.slice(1)}Storage(cfg);`;
604
+ src = src.replace(STORAGE_BRANCH, chosenReturn);
551
605
  await writeFile(modulePath, src);
552
606
  } catch {
553
607
  }
@@ -557,14 +611,15 @@ async function removeUnusedDbStrategies(targetDir, dbProvider) {
557
611
  if (dbProvider === "supabase") {
558
612
  await rm(join2(targetDir, "libs/db-strategies/firestore"), { recursive: true, force: true });
559
613
  await stripDeps(join2(targetDir, "apps/microservices/notes/package.json"), [
560
- "@icore/db-firestore"
614
+ "@icore/db-firestore",
615
+ "@icore/firebase-admin"
561
616
  ]);
562
617
  await stripTsconfigPath(targetDir, "@icore/db-firestore");
563
618
  try {
564
619
  const src = await readFile2(modulePath, "utf8");
565
- const next = src.replace(/^import \* as admin from 'firebase-admin';\n/m, "").replace(/^import \{[^}]*FirestoreDBStrategy[^}]*\} from '@icore\/db-firestore';\n/m, "").replace(
566
- /\n {8}if \(provider === 'firestore'[\s\S]*?return new FirestoreDBStrategy\(\{[\s\S]*?\}\);\n {8}\}\n/m,
567
- ""
620
+ const next = src.replace(/^import \{[^}]*\} from '@icore\/firebase-admin';\n/m, "").replace(/^import \{[^}]*FirestoreDBStrategy[^}]*\} from '@icore\/db-firestore';\n/m, "").replace(/^ {2}firestore: \[[^\]]*\],\n/m, "").replace(/^ {2}firebase: \[[^\]]*\],\n/m, "").replace(/\nfunction makeFirestoreDB[\s\S]*?\n}\n/m, "").replace(
621
+ /if \(provider === 'supabase'\) return makeSupabaseDB\(cfg\);\n\s*return makeFirestoreDB\(cfg\);/m,
622
+ "return makeSupabaseDB(cfg);"
568
623
  );
569
624
  await writeFile(modulePath, next);
570
625
  } catch {
@@ -578,15 +633,19 @@ async function removeUnusedDbStrategies(targetDir, dbProvider) {
578
633
  await stripTsconfigPath(targetDir, "@icore/db-supabase");
579
634
  try {
580
635
  const src = await readFile2(modulePath, "utf8");
581
- const next = src.replace(/^import \{ createClient \} from '@supabase\/supabase-js';\n/m, "").replace(/^import \{[^}]*SupabaseDBStrategy[^}]*\} from '@icore\/db-supabase';\n/m, "").replace(
582
- /\n {8}if \(provider === 'supabase'\) \{[\s\S]*?return new SupabaseDBStrategy\(\{ client \}\);\n {8}\}\n/m,
583
- ""
636
+ const next = src.replace(/^import \{ createClient \} from '@supabase\/supabase-js';\n/m, "").replace(/^import \{[^}]*SupabaseDBStrategy[^}]*\} from '@icore\/db-supabase';\n/m, "").replace(/\nfunction makeSupabaseDB[\s\S]*?\n}\n/m, "").replace(
637
+ /if \(provider === 'supabase'\) return makeSupabaseDB\(cfg\);\n\s*return makeFirestoreDB\(cfg\);/m,
638
+ "return makeFirestoreDB(cfg);"
584
639
  );
585
640
  await writeFile(modulePath, next);
586
641
  } catch {
587
642
  }
588
643
  }
589
644
  }
645
+ async function removeFirebaseAdminLib(targetDir) {
646
+ await rm(join2(targetDir, "libs/firebase-admin"), { recursive: true, force: true });
647
+ await stripTsconfigPath(targetDir, "@icore/firebase-admin");
648
+ }
590
649
  async function removeUploadStack(targetDir) {
591
650
  const paths = [
592
651
  "apps/microservices/upload",
@@ -691,6 +750,7 @@ async function scaffold(opts, templatesDir2) {
691
750
  await writeGatewayEnv(opts.targetDir, opts);
692
751
  await writeRootEnv(opts.targetDir, opts);
693
752
  await selectClientTemplate(opts.targetDir, opts);
753
+ await writeClientEnv(opts.targetDir);
694
754
  if (opts.upload === "none") await removeUploadStack(opts.targetDir);
695
755
  if (opts.payment === "none") await removePaymentStack(opts.targetDir);
696
756
  if (opts.jobs === "none") await removeJobsStack(opts.targetDir);
@@ -698,12 +758,230 @@ async function scaffold(opts, templatesDir2) {
698
758
  await removeUnusedAuthStrategies(opts.targetDir, opts.authProvider);
699
759
  await removeUnusedStorageStrategies(opts.targetDir, opts.upload);
700
760
  await removeUnusedDbStrategies(opts.targetDir, opts.dbProvider);
761
+ const firebaseUsed = opts.authProvider === "firebase" || opts.dbProvider === "firebase" || opts.upload === "firebase";
762
+ if (!firebaseUsed) await removeFirebaseAdminLib(opts.targetDir);
701
763
  if (opts.packageManager === "yarn") {
702
764
  await writeFile(join2(opts.targetDir, "yarn.lock"), "");
765
+ } else {
766
+ await rm(join2(opts.targetDir, ".yarn"), { recursive: true, force: true });
767
+ await rm(join2(opts.targetDir, ".yarnrc.yml"), { force: true });
703
768
  }
769
+ await patchGitignoreForPm(opts.targetDir, opts.packageManager);
770
+ await writeAiFiles(opts.targetDir, opts);
704
771
  if (opts.install) runInstall(opts.targetDir, opts.packageManager);
705
772
  if (opts.initGit) gitInit(opts.targetDir, opts.projectName);
706
773
  }
774
+ async function patchGitignoreForPm(targetDir, pm) {
775
+ const giPath = join2(targetDir, ".gitignore");
776
+ try {
777
+ let src = await readFile2(giPath, "utf8");
778
+ src = src.replace(/^# Build artifacts.*\ntools\/create-icore\/templates\/\s*\n/m, "");
779
+ if (pm !== "yarn") {
780
+ src = src.replace(/^\.yarn\/\*\s*\n/m, "").replace(/^!\.yarn\/patches\s*\n/m, "").replace(/^!\.yarn\/plugins\s*\n/m, "").replace(/^!\.yarn\/releases\s*\n/m, "").replace(/^!\.yarn\/sdks\s*\n/m, "").replace(/^!\.yarn\/versions\s*\n/m, "").replace(/^\.pnp\.\*\s*\n/m, "");
781
+ }
782
+ if (pm === "pnpm") {
783
+ if (!src.includes(".pnpm-debug.log")) {
784
+ src += "\n# pnpm\n.pnpm-debug.log*\n";
785
+ }
786
+ }
787
+ if (pm === "npm") {
788
+ if (!src.includes("npm-debug.log")) {
789
+ src += "\n# npm\nnpm-debug.log*\n";
790
+ }
791
+ }
792
+ await writeFile(giPath, src);
793
+ } catch {
794
+ }
795
+ }
796
+ async function writeAiFiles(targetDir, opts) {
797
+ const pm = opts.packageManager;
798
+ const nx = pm === "npm" ? "npx nx" : `${pm} nx`;
799
+ const devCmd = pmRun(pm, "dev");
800
+ const activeMSes = ["auth (port 4001)"];
801
+ if (opts.upload !== "none") activeMSes.push(`upload (port 4002)`);
802
+ if (opts.payment !== "none") activeMSes.push(`payment (port 4003)`);
803
+ if (opts.example !== "none") activeMSes.push(`notes (port 4004)`);
804
+ if (opts.jobs !== "none") activeMSes.push(`jobs (standalone)`);
805
+ const usesSupabase = opts.authProvider === "supabase" || opts.dbProvider === "supabase" || opts.upload === "supabase";
806
+ const usesFirebase = opts.authProvider === "firebase" || opts.dbProvider === "firebase" || opts.upload === "firebase";
807
+ await writeFile(join2(targetDir, "CLAUDE.md"), "@AGENTS.md\n");
808
+ const uiLabel = { shadcn: "shadcn/ui + Tailwind", antd: "Ant Design 6", mui: "MUI 6" }[opts.ui];
809
+ const readme = `# ${opts.projectName}
810
+
811
+ > Scaffolded with [iCore](https://github.com/iDEVconn/create-icore) \u2014 Nx + NestJS + React full-stack template.
812
+
813
+ ## Stack
814
+
815
+ | Layer | Technology |
816
+ |-------|-----------|
817
+ | Monorepo | Nx + ${pm} |
818
+ | Gateway | NestJS 11 + Swagger |
819
+ | Auth | ${opts.authProvider} |
820
+ | Database | ${opts.dbProvider} |
821
+ | Upload | ${opts.upload === "none" ? "\u2014" : opts.upload} |
822
+ | UI | ${uiLabel} + TanStack Router + Query |
823
+ | i18n | i18next (en / ru / he) |
824
+
825
+ ## Quick start
826
+
827
+ \`\`\`bash
828
+ # 1. Fill in provider credentials
829
+ # apps/microservices/auth/.env
830
+ # apps/microservices/upload/.env (if upload is enabled)
831
+ # apps/client/.env (VITE_API_URL \u2014 already defaults to /api)
832
+
833
+ # 2. Start everything
834
+ ${devCmd}
835
+ # \u2192 http://localhost:4200 client
836
+ # \u2192 http://localhost:3001/api/docs Swagger
837
+ \`\`\`
838
+
839
+ ## Commands
840
+
841
+ \`\`\`bash
842
+ ${nx} run <project>:serve # start a single service
843
+ ${nx} test <project> # unit tests
844
+ ${nx} lint <project> # lint
845
+ ${nx} build <project> # production build
846
+ ${pm === "yarn" ? "yarn remove-notes" : pm === "pnpm" ? "pnpm remove-notes" : "npm run remove-notes"} # strip the notes sample feature
847
+ \`\`\`
848
+
849
+ ## Scaffolded by
850
+
851
+ [iCore](https://github.com/iDEVconn/create-icore) \u2014 [@idevconn/create-icore](https://www.npmjs.com/package/@idevconn/create-icore)
852
+
853
+ ## License
854
+
855
+ Apache-2.0
856
+ `;
857
+ await writeFile(join2(targetDir, "README.md"), readme);
858
+ const agents = `# ${opts.projectName} \u2014 Agent Instructions
859
+
860
+ ## Stack snapshot
861
+
862
+ | Dimension | Choice |
863
+ |------------|--------|
864
+ | Auth | ${opts.authProvider} |
865
+ | Database | ${opts.dbProvider} |
866
+ | Upload | ${opts.upload} |
867
+ | Payment | ${opts.payment} |
868
+ | Jobs | ${opts.jobs} |
869
+ | UI | ${opts.ui} |
870
+ | Transport | ${opts.transport} |
871
+ | PM | ${pm} |
872
+
873
+ ## \u{1F680} Mandatory Workflow
874
+
875
+ - **Branch strategy**: \`dev\` is default. Cut \`feature/<name>\` or \`bug/<name>\` from dev. PRs only target dev. Never push directly to main.
876
+ - **No code without approval**: Propose changes first, wait for go-ahead.
877
+ - **\u0417\u0410\u041A\u041E\u041D \u2014 no crash on missing .env**: MS factories must catch config errors, print a boxed banner with ALL missing vars, and return a Fake strategy in dev. In prod (\`NODE_ENV=production\`) throw the same banner. The \`formatEnvBanner\` + \`missingEnv\` helpers from \`@icore/shared\` handle this.
878
+ - **Post-coding routine**: \`npx prettier --write <files>\` \u2192 \`${nx} lint <project>\` \u2192 \`${nx} build <project>\` \u2014 all green before committing.
879
+ - **Nx generators only**: never hand-write \`project.json\` / tsconfig stacks. Use \`${nx} g @nx/<plugin>:<schematic>\`.
880
+
881
+ ## Architecture
882
+
883
+ \`\`\`
884
+ apps/
885
+ \u251C\u2500\u2500 api/ NestJS gateway \u2014 all client traffic enters here (:3001)
886
+ \u251C\u2500\u2500 microservices/
887
+ ${activeMSes.map((s) => `\u2502 \u251C\u2500\u2500 ${s.split(" ")[0]}/`).join("\n")}
888
+ \u2514\u2500\u2500 client/ Vite + React 19 + ${opts.ui} (:4200)
889
+ libs/
890
+ \u251C\u2500\u2500 shared/ contracts, CASL, transport helpers, env banner utils
891
+ \u251C\u2500\u2500 auth-strategies/${opts.authProvider}/
892
+ ${opts.upload !== "none" ? `\u251C\u2500\u2500 storage-strategies/${opts.upload}/
893
+ ` : ""}\u251C\u2500\u2500 db-strategies/${opts.dbProvider === "firebase" ? "firestore" : opts.dbProvider}/
894
+ \u251C\u2500\u2500 auth-client/ gateway \u2192 auth MS (TCP/Redis/NATS)
895
+ ${opts.upload !== "none" ? `\u251C\u2500\u2500 upload-client/ gateway \u2192 upload MS
896
+ ` : ""}\u2514\u2500\u2500 template-shared/ browser-safe React foundation (stores, i18n, CASL)
897
+ \`\`\`
898
+
899
+ ## Key patterns
900
+
901
+ **Strategy swap** \u2014 provider is chosen at runtime via env. Never import a concrete strategy in app code; always inject via the factory token (\`AuthStrategy\`, \`StorageStrategy\`, \`DBStrategy\`).
902
+
903
+ **Transport** \u2014 \`buildTransport(prefix)\` reads \`${opts.transport.toUpperCase()}*\` vars. Same helper on gateway client-modules and each MS \`main.ts\`. Supports tcp / redis / nats \u2014 change by flipping \`*_TRANSPORT\` in \`.env\`.
904
+
905
+ **Env layering**:
906
+ 1. Root \`.env\` \u2014 \`DB_PROVIDER\`
907
+ 2. \`apps/api/.env\` \u2014 gateway transport endpoints
908
+ 3. \`apps/microservices/<name>/.env\` \u2014 each MS provider + transport
909
+ 4. \`apps/client/.env\` \u2014 \`VITE_API_URL\`
910
+
911
+ ## Commands
912
+
913
+ \`\`\`bash
914
+ ${devCmd} # start all services
915
+ ${nx} run api:serve # gateway only
916
+ ${nx} run auth:serve # auth MS only
917
+ ${nx} test <project> # run tests
918
+ ${nx} lint <project> # lint
919
+ ${nx} build <project> # build
920
+ ${nx} g @nx/nest:resource # generate NestJS resource
921
+ \`\`\`
922
+
923
+ ## .env files to configure
924
+
925
+ | File | Key vars |
926
+ |------|----------|
927
+ | \`apps/microservices/auth/.env\` | \`AUTH_PROVIDER=${opts.authProvider}\`, ${opts.authProvider === "supabase" ? "`SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`" : "`FB_ADMIN_*`, `FIREBASE_WEB_API_KEY`"} |
928
+ ${opts.upload !== "none" ? `| \`apps/microservices/upload/.env\` | \`STORAGE_PROVIDER=${opts.upload}\`, provider creds |
929
+ ` : ""}| \`apps/microservices/notes/.env\` | \`DB_PROVIDER=${opts.dbProvider}\`, DB creds |
930
+ | \`apps/client/.env\` | \`VITE_API_URL=/api\` (proxied to :3001 in dev) |
931
+
932
+ ## Testing
933
+
934
+ - Unit tests: Vitest, files named \`*.unit.test.ts(x)\` in \`__tests__/\` next to source.
935
+ - Test behaviour, not implementation. Fake strategies from \`@icore/shared\` (FakeAuthStrategy etc.) serve as test doubles.
936
+ - Run: \`${nx} test <project>\`
937
+ `;
938
+ await writeFile(join2(targetDir, "AGENTS.md"), agents);
939
+ await mkdir(join2(targetDir, ".claude"), { recursive: true });
940
+ const mcpServers = {
941
+ nx: {
942
+ command: "npx",
943
+ args: ["-y", "@nx/mcp@latest", "--directory", "."],
944
+ type: "stdio"
945
+ }
946
+ };
947
+ if (usesSupabase) {
948
+ mcpServers["supabase"] = {
949
+ command: "npx",
950
+ args: [
951
+ "-y",
952
+ "@supabase/mcp-server-supabase@latest",
953
+ "--access-token",
954
+ "<SUPABASE_PERSONAL_ACCESS_TOKEN>"
955
+ ],
956
+ type: "stdio"
957
+ };
958
+ }
959
+ if (usesFirebase) {
960
+ mcpServers["firebase"] = {
961
+ command: "npx",
962
+ args: ["-y", "firebase-tools@latest", "experimental:mcp"],
963
+ type: "stdio"
964
+ };
965
+ }
966
+ const nxCmds = [`Bash(${nx} *)`, `Bash(${devCmd})`];
967
+ if (pm !== "npm") nxCmds.push(`Bash(npx nx *)`);
968
+ const settings = {
969
+ mcpServers,
970
+ permissions: {
971
+ allow: [
972
+ ...nxCmds,
973
+ "Bash(npx prettier *)",
974
+ "Bash(git status)",
975
+ "Bash(git diff *)",
976
+ "Bash(git log *)"
977
+ ]
978
+ }
979
+ };
980
+ await writeFile(
981
+ join2(targetDir, ".claude", "settings.json"),
982
+ JSON.stringify(settings, null, 2) + "\n"
983
+ );
984
+ }
707
985
 
708
986
  // src/cli.ts
709
987
  var [nodeMajor] = process.versions.node.split(".").map(Number);
@@ -735,7 +1013,9 @@ async function main() {
735
1013
  p2.log.info(`Next:`);
736
1014
  p2.log.info(` cd ${opts.projectName}`);
737
1015
  if (!opts.install) p2.log.info(` ${opts.packageManager} install`);
738
- p2.log.info(` ${opts.packageManager} dev # gateway + auth MS + upload MS + client`);
1016
+ p2.log.info(
1017
+ ` ${pmRun(opts.packageManager, "dev")} # gateway + auth MS + upload MS + client`
1018
+ );
739
1019
  p2.log.info(` open http://localhost:4200`);
740
1020
  p2.log.info(` edit apps/microservices/auth/.env to plug in real ${opts.authProvider} creds`);
741
1021
  }