@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/index.js CHANGED
@@ -1,3 +1,8 @@
1
+ // src/lib/options.ts
2
+ function pmRun(pm, script) {
3
+ return pm === "npm" ? `npm run ${script}` : `${pm} ${script}`;
4
+ }
5
+
1
6
  // src/lib/scaffold.ts
2
7
  import { copyFile, mkdir, readdir, readFile, stat, writeFile, rm } from "fs/promises";
3
8
  import { readFileSync } from "fs";
@@ -35,9 +40,29 @@ async function rewriteRootPackageJson(targetDir, opts) {
35
40
  pkg["version"] = "0.0.1";
36
41
  pkg["private"] = true;
37
42
  delete pkg.description;
43
+ if (opts.transport === "nats") {
44
+ const deps = pkg["dependencies"] ??= {};
45
+ deps["nats"] = "^2.29.3";
46
+ }
38
47
  if (opts.packageManager !== "yarn") {
39
48
  delete pkg.packageManager;
40
49
  }
50
+ if (opts.packageManager === "pnpm") {
51
+ pkg["pnpm"] = {
52
+ onlyBuiltDependencies: [
53
+ "@firebase/util",
54
+ "@nestjs/core",
55
+ "@parcel/watcher",
56
+ "@scarf/scarf",
57
+ "@swc/core",
58
+ "less",
59
+ "msgpackr-extract",
60
+ "nx",
61
+ "protobufjs",
62
+ "unrs-resolver"
63
+ ]
64
+ };
65
+ }
41
66
  await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
42
67
  }
43
68
  async function writeAuthEnv(targetDir, opts) {
@@ -75,9 +100,9 @@ async function writeNotesEnv(targetDir, opts) {
75
100
  async function writeGatewayEnv(targetDir, opts) {
76
101
  const envExample = join(targetDir, "apps/api/.env.example");
77
102
  const env = await readFile(envExample, "utf8");
78
- let next = env.replace(/^AUTH_TRANSPORT=.*$/m, `AUTH_TRANSPORT=${opts.transport}`).replace(/^UPLOAD_TRANSPORT=.*$/m, `UPLOAD_TRANSPORT=${opts.transport}`);
103
+ 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}`);
79
104
  if (opts.transport !== "tcp") {
80
- next = next.replace(/^# (AUTH_(?:REDIS|NATS)_URL=)/m, "$1").replace(/^# (UPLOAD_(?:REDIS|NATS)_URL=)/m, "$1");
105
+ 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");
81
106
  }
82
107
  await writeFile(join(targetDir, "apps/api/.env"), next);
83
108
  }
@@ -90,6 +115,25 @@ async function writeRootEnv(targetDir, opts) {
90
115
  ];
91
116
  await writeFile(join(targetDir, ".env"), lines.join("\n"));
92
117
  }
118
+ async function stripGatewayTransport(targetDir, prefix) {
119
+ const gatewayEnv = join(targetDir, "apps/api/.env");
120
+ try {
121
+ const env = await readFile(gatewayEnv, "utf8");
122
+ const next = env.split("\n").filter(
123
+ (line) => !line.startsWith(`${prefix}_`) && !line.startsWith(`# ${prefix}_`) && !line.includes(`${prefix} MS transport`)
124
+ ).join("\n");
125
+ await writeFile(gatewayEnv, next);
126
+ } catch {
127
+ }
128
+ }
129
+ async function writeClientEnv(targetDir) {
130
+ const envExample = join(targetDir, "apps/client/.env.example");
131
+ try {
132
+ const env = await readFile(envExample, "utf8");
133
+ await writeFile(join(targetDir, "apps/client/.env"), env);
134
+ } catch {
135
+ }
136
+ }
93
137
  async function writePaymentEnv(targetDir, opts) {
94
138
  if (opts.payment === "none") return;
95
139
  const envExample = join(targetDir, "apps/microservices/payment/.env.example");
@@ -166,6 +210,7 @@ async function removePaymentStack(targetDir) {
166
210
  "@icore/payment-client",
167
211
  "@idevconn/payment"
168
212
  ]);
213
+ await stripGatewayTransport(targetDir, "PAYMENT");
169
214
  }
170
215
  async function removeNotesStack(targetDir) {
171
216
  for (const p2 of [
@@ -187,6 +232,7 @@ async function removeNotesStack(targetDir) {
187
232
  } catch {
188
233
  }
189
234
  await stripDeps(join(targetDir, "apps/api/package.json"), ["@icore/notes-client"]);
235
+ await stripGatewayTransport(targetDir, "NOTES");
190
236
  const tsconfigPath = join(targetDir, "tsconfig.base.json");
191
237
  try {
192
238
  const src = await readFile(tsconfigPath, "utf8");
@@ -243,15 +289,17 @@ async function stripTsconfigPath(targetDir, alias) {
243
289
  }
244
290
  async function removeUnusedAuthStrategies(targetDir, authProvider) {
245
291
  const modulePath = join(targetDir, "apps/microservices/auth/src/app/app.module.ts");
292
+ const AUTH_BRANCH = /if \(provider === 'supabase'\) return makeSupabaseAuth\(cfg\);\n\s*return makeFirebaseAuth\(cfg\);/m;
246
293
  if (authProvider === "supabase") {
247
294
  await rm(join(targetDir, "libs/auth-strategies/firebase"), { recursive: true, force: true });
248
295
  await stripDeps(join(targetDir, "apps/microservices/auth/package.json"), [
249
- "@icore/auth-firebase"
296
+ "@icore/auth-firebase",
297
+ "@icore/firebase-admin"
250
298
  ]);
251
299
  await stripTsconfigPath(targetDir, "@icore/auth-firebase");
252
300
  try {
253
301
  const src = await readFile(modulePath, "utf8");
254
- 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/, "");
302
+ 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);");
255
303
  await writeFile(modulePath, next);
256
304
  } catch {
257
305
  }
@@ -264,10 +312,7 @@ async function removeUnusedAuthStrategies(targetDir, authProvider) {
264
312
  await stripTsconfigPath(targetDir, "@icore/auth-supabase");
265
313
  try {
266
314
  const src = await readFile(modulePath, "utf8");
267
- const next = src.replace(/^import \{ createClient \} from '@supabase\/supabase-js';\n/m, "").replace(/^import \{[^}]*SupabaseAuthStrategy[^}]*\} from '@icore\/auth-supabase';\n/m, "").replace(
268
- /\n {10}case 'supabase': \{[\s\S]*?return new SupabaseAuthStrategy\(\{ client \}\);\n {10}\}\n/m,
269
- ""
270
- );
315
+ 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);");
271
316
  await writeFile(modulePath, next);
272
317
  } catch {
273
318
  }
@@ -279,7 +324,8 @@ async function removeUnusedStorageStrategies(targetDir, uploadProvider) {
279
324
  if (uploadProvider !== "firebase") {
280
325
  await rm(join(targetDir, "libs/storage-strategies/firebase"), { recursive: true, force: true });
281
326
  await stripDeps(join(targetDir, "apps/microservices/upload/package.json"), [
282
- "@icore/storage-firebase"
327
+ "@icore/storage-firebase",
328
+ "@icore/firebase-admin"
283
329
  ]);
284
330
  await stripTsconfigPath(targetDir, "@icore/storage-firebase");
285
331
  }
@@ -303,26 +349,26 @@ async function removeUnusedStorageStrategies(targetDir, uploadProvider) {
303
349
  try {
304
350
  let src = await readFile(modulePath, "utf8");
305
351
  if (uploadProvider !== "firebase") {
306
- src = src.replace(/^import \* as admin from 'firebase-admin';\n/m, "").replace(
352
+ src = src.replace(/^import \{[^}]*\} from '@icore\/firebase-admin';\n/m, "").replace(
307
353
  /^import \{[^}]*FirebaseStorageStrategy[^}]*\} from '@icore\/storage-firebase';\n/m,
308
354
  ""
309
- ).replace(/^function makeFirebaseStorage\b[\s\S]*?\n^}\n/m, "").replace(/(?<=\n) *case 'firebase':\n *return makeFirebaseStorage\(cfg\);\n/, "");
355
+ ).replace(/^ {2}firebase: \[[^\]]*\],\n/m, "").replace(/\nfunction makeFirebaseStorage[\s\S]*?\n}\n/m, "");
310
356
  }
311
357
  if (uploadProvider !== "cloudinary") {
312
358
  src = src.replace(/^import \{ v2 as cloudinary \} from 'cloudinary';\n/m, "").replace(
313
359
  /^import \{[^}]*CloudinaryStorageStrategy[^}]*\} from '@icore\/storage-cloudinary';\n/m,
314
360
  ""
315
- ).replace(/^function makeCloudinaryStorage\b[\s\S]*?\n^}\n/m, "").replace(/(?<=\n) *case 'cloudinary':\n *return makeCloudinaryStorage\(cfg\);\n/, "");
361
+ ).replace(/\nfunction makeCloudinaryStorage[\s\S]*?\n}\n/m, "");
316
362
  }
317
363
  if (uploadProvider !== "supabase") {
318
364
  src = src.replace(/^import \{ createClient \} from '@supabase\/supabase-js';\n/m, "").replace(
319
365
  /^import \{[^}]*SupabaseStorageStrategy[^}]*\} from '@icore\/storage-supabase';\n/m,
320
366
  ""
321
- ).replace(
322
- /\n {10}case 'supabase': \{[\s\S]*?bucket: requireEnv\(cfg, 'SUPABASE_STORAGE_BUCKET'\),\n {12}\}\);\n {10}\}\n/m,
323
- ""
324
- );
367
+ ).replace(/\nfunction makeSupabaseStorage[\s\S]*?\n}\n/m, "");
325
368
  }
369
+ const STORAGE_BRANCH = /if \(provider === 'supabase'\) return makeSupabaseStorage\(cfg\);\n\s*if \(provider === 'firebase'\) return makeFirebaseStorage\(cfg\);\n\s*return makeCloudinaryStorage\(cfg\);/m;
370
+ const chosenReturn = `return make${uploadProvider.charAt(0).toUpperCase() + uploadProvider.slice(1)}Storage(cfg);`;
371
+ src = src.replace(STORAGE_BRANCH, chosenReturn);
326
372
  await writeFile(modulePath, src);
327
373
  } catch {
328
374
  }
@@ -332,14 +378,15 @@ async function removeUnusedDbStrategies(targetDir, dbProvider) {
332
378
  if (dbProvider === "supabase") {
333
379
  await rm(join(targetDir, "libs/db-strategies/firestore"), { recursive: true, force: true });
334
380
  await stripDeps(join(targetDir, "apps/microservices/notes/package.json"), [
335
- "@icore/db-firestore"
381
+ "@icore/db-firestore",
382
+ "@icore/firebase-admin"
336
383
  ]);
337
384
  await stripTsconfigPath(targetDir, "@icore/db-firestore");
338
385
  try {
339
386
  const src = await readFile(modulePath, "utf8");
340
- const next = src.replace(/^import \* as admin from 'firebase-admin';\n/m, "").replace(/^import \{[^}]*FirestoreDBStrategy[^}]*\} from '@icore\/db-firestore';\n/m, "").replace(
341
- /\n {8}if \(provider === 'firestore'[\s\S]*?return new FirestoreDBStrategy\(\{[\s\S]*?\}\);\n {8}\}\n/m,
342
- ""
387
+ 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(
388
+ /if \(provider === 'supabase'\) return makeSupabaseDB\(cfg\);\n\s*return makeFirestoreDB\(cfg\);/m,
389
+ "return makeSupabaseDB(cfg);"
343
390
  );
344
391
  await writeFile(modulePath, next);
345
392
  } catch {
@@ -353,15 +400,19 @@ async function removeUnusedDbStrategies(targetDir, dbProvider) {
353
400
  await stripTsconfigPath(targetDir, "@icore/db-supabase");
354
401
  try {
355
402
  const src = await readFile(modulePath, "utf8");
356
- const next = src.replace(/^import \{ createClient \} from '@supabase\/supabase-js';\n/m, "").replace(/^import \{[^}]*SupabaseDBStrategy[^}]*\} from '@icore\/db-supabase';\n/m, "").replace(
357
- /\n {8}if \(provider === 'supabase'\) \{[\s\S]*?return new SupabaseDBStrategy\(\{ client \}\);\n {8}\}\n/m,
358
- ""
403
+ 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(
404
+ /if \(provider === 'supabase'\) return makeSupabaseDB\(cfg\);\n\s*return makeFirestoreDB\(cfg\);/m,
405
+ "return makeFirestoreDB(cfg);"
359
406
  );
360
407
  await writeFile(modulePath, next);
361
408
  } catch {
362
409
  }
363
410
  }
364
411
  }
412
+ async function removeFirebaseAdminLib(targetDir) {
413
+ await rm(join(targetDir, "libs/firebase-admin"), { recursive: true, force: true });
414
+ await stripTsconfigPath(targetDir, "@icore/firebase-admin");
415
+ }
365
416
  async function removeUploadStack(targetDir) {
366
417
  const paths = [
367
418
  "apps/microservices/upload",
@@ -466,6 +517,7 @@ async function scaffold(opts, templatesDir) {
466
517
  await writeGatewayEnv(opts.targetDir, opts);
467
518
  await writeRootEnv(opts.targetDir, opts);
468
519
  await selectClientTemplate(opts.targetDir, opts);
520
+ await writeClientEnv(opts.targetDir);
469
521
  if (opts.upload === "none") await removeUploadStack(opts.targetDir);
470
522
  if (opts.payment === "none") await removePaymentStack(opts.targetDir);
471
523
  if (opts.jobs === "none") await removeJobsStack(opts.targetDir);
@@ -473,12 +525,230 @@ async function scaffold(opts, templatesDir) {
473
525
  await removeUnusedAuthStrategies(opts.targetDir, opts.authProvider);
474
526
  await removeUnusedStorageStrategies(opts.targetDir, opts.upload);
475
527
  await removeUnusedDbStrategies(opts.targetDir, opts.dbProvider);
528
+ const firebaseUsed = opts.authProvider === "firebase" || opts.dbProvider === "firebase" || opts.upload === "firebase";
529
+ if (!firebaseUsed) await removeFirebaseAdminLib(opts.targetDir);
476
530
  if (opts.packageManager === "yarn") {
477
531
  await writeFile(join(opts.targetDir, "yarn.lock"), "");
532
+ } else {
533
+ await rm(join(opts.targetDir, ".yarn"), { recursive: true, force: true });
534
+ await rm(join(opts.targetDir, ".yarnrc.yml"), { force: true });
478
535
  }
536
+ await patchGitignoreForPm(opts.targetDir, opts.packageManager);
537
+ await writeAiFiles(opts.targetDir, opts);
479
538
  if (opts.install) runInstall(opts.targetDir, opts.packageManager);
480
539
  if (opts.initGit) gitInit(opts.targetDir, opts.projectName);
481
540
  }
541
+ async function patchGitignoreForPm(targetDir, pm) {
542
+ const giPath = join(targetDir, ".gitignore");
543
+ try {
544
+ let src = await readFile(giPath, "utf8");
545
+ src = src.replace(/^# Build artifacts.*\ntools\/create-icore\/templates\/\s*\n/m, "");
546
+ if (pm !== "yarn") {
547
+ 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, "");
548
+ }
549
+ if (pm === "pnpm") {
550
+ if (!src.includes(".pnpm-debug.log")) {
551
+ src += "\n# pnpm\n.pnpm-debug.log*\n";
552
+ }
553
+ }
554
+ if (pm === "npm") {
555
+ if (!src.includes("npm-debug.log")) {
556
+ src += "\n# npm\nnpm-debug.log*\n";
557
+ }
558
+ }
559
+ await writeFile(giPath, src);
560
+ } catch {
561
+ }
562
+ }
563
+ async function writeAiFiles(targetDir, opts) {
564
+ const pm = opts.packageManager;
565
+ const nx = pm === "npm" ? "npx nx" : `${pm} nx`;
566
+ const devCmd = pmRun(pm, "dev");
567
+ const activeMSes = ["auth (port 4001)"];
568
+ if (opts.upload !== "none") activeMSes.push(`upload (port 4002)`);
569
+ if (opts.payment !== "none") activeMSes.push(`payment (port 4003)`);
570
+ if (opts.example !== "none") activeMSes.push(`notes (port 4004)`);
571
+ if (opts.jobs !== "none") activeMSes.push(`jobs (standalone)`);
572
+ const usesSupabase = opts.authProvider === "supabase" || opts.dbProvider === "supabase" || opts.upload === "supabase";
573
+ const usesFirebase = opts.authProvider === "firebase" || opts.dbProvider === "firebase" || opts.upload === "firebase";
574
+ await writeFile(join(targetDir, "CLAUDE.md"), "@AGENTS.md\n");
575
+ const uiLabel = { shadcn: "shadcn/ui + Tailwind", antd: "Ant Design 6", mui: "MUI 6" }[opts.ui];
576
+ const readme = `# ${opts.projectName}
577
+
578
+ > Scaffolded with [iCore](https://github.com/iDEVconn/create-icore) \u2014 Nx + NestJS + React full-stack template.
579
+
580
+ ## Stack
581
+
582
+ | Layer | Technology |
583
+ |-------|-----------|
584
+ | Monorepo | Nx + ${pm} |
585
+ | Gateway | NestJS 11 + Swagger |
586
+ | Auth | ${opts.authProvider} |
587
+ | Database | ${opts.dbProvider} |
588
+ | Upload | ${opts.upload === "none" ? "\u2014" : opts.upload} |
589
+ | UI | ${uiLabel} + TanStack Router + Query |
590
+ | i18n | i18next (en / ru / he) |
591
+
592
+ ## Quick start
593
+
594
+ \`\`\`bash
595
+ # 1. Fill in provider credentials
596
+ # apps/microservices/auth/.env
597
+ # apps/microservices/upload/.env (if upload is enabled)
598
+ # apps/client/.env (VITE_API_URL \u2014 already defaults to /api)
599
+
600
+ # 2. Start everything
601
+ ${devCmd}
602
+ # \u2192 http://localhost:4200 client
603
+ # \u2192 http://localhost:3001/api/docs Swagger
604
+ \`\`\`
605
+
606
+ ## Commands
607
+
608
+ \`\`\`bash
609
+ ${nx} run <project>:serve # start a single service
610
+ ${nx} test <project> # unit tests
611
+ ${nx} lint <project> # lint
612
+ ${nx} build <project> # production build
613
+ ${pm === "yarn" ? "yarn remove-notes" : pm === "pnpm" ? "pnpm remove-notes" : "npm run remove-notes"} # strip the notes sample feature
614
+ \`\`\`
615
+
616
+ ## Scaffolded by
617
+
618
+ [iCore](https://github.com/iDEVconn/create-icore) \u2014 [@idevconn/create-icore](https://www.npmjs.com/package/@idevconn/create-icore)
619
+
620
+ ## License
621
+
622
+ Apache-2.0
623
+ `;
624
+ await writeFile(join(targetDir, "README.md"), readme);
625
+ const agents = `# ${opts.projectName} \u2014 Agent Instructions
626
+
627
+ ## Stack snapshot
628
+
629
+ | Dimension | Choice |
630
+ |------------|--------|
631
+ | Auth | ${opts.authProvider} |
632
+ | Database | ${opts.dbProvider} |
633
+ | Upload | ${opts.upload} |
634
+ | Payment | ${opts.payment} |
635
+ | Jobs | ${opts.jobs} |
636
+ | UI | ${opts.ui} |
637
+ | Transport | ${opts.transport} |
638
+ | PM | ${pm} |
639
+
640
+ ## \u{1F680} Mandatory Workflow
641
+
642
+ - **Branch strategy**: \`dev\` is default. Cut \`feature/<name>\` or \`bug/<name>\` from dev. PRs only target dev. Never push directly to main.
643
+ - **No code without approval**: Propose changes first, wait for go-ahead.
644
+ - **\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.
645
+ - **Post-coding routine**: \`npx prettier --write <files>\` \u2192 \`${nx} lint <project>\` \u2192 \`${nx} build <project>\` \u2014 all green before committing.
646
+ - **Nx generators only**: never hand-write \`project.json\` / tsconfig stacks. Use \`${nx} g @nx/<plugin>:<schematic>\`.
647
+
648
+ ## Architecture
649
+
650
+ \`\`\`
651
+ apps/
652
+ \u251C\u2500\u2500 api/ NestJS gateway \u2014 all client traffic enters here (:3001)
653
+ \u251C\u2500\u2500 microservices/
654
+ ${activeMSes.map((s) => `\u2502 \u251C\u2500\u2500 ${s.split(" ")[0]}/`).join("\n")}
655
+ \u2514\u2500\u2500 client/ Vite + React 19 + ${opts.ui} (:4200)
656
+ libs/
657
+ \u251C\u2500\u2500 shared/ contracts, CASL, transport helpers, env banner utils
658
+ \u251C\u2500\u2500 auth-strategies/${opts.authProvider}/
659
+ ${opts.upload !== "none" ? `\u251C\u2500\u2500 storage-strategies/${opts.upload}/
660
+ ` : ""}\u251C\u2500\u2500 db-strategies/${opts.dbProvider === "firebase" ? "firestore" : opts.dbProvider}/
661
+ \u251C\u2500\u2500 auth-client/ gateway \u2192 auth MS (TCP/Redis/NATS)
662
+ ${opts.upload !== "none" ? `\u251C\u2500\u2500 upload-client/ gateway \u2192 upload MS
663
+ ` : ""}\u2514\u2500\u2500 template-shared/ browser-safe React foundation (stores, i18n, CASL)
664
+ \`\`\`
665
+
666
+ ## Key patterns
667
+
668
+ **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\`).
669
+
670
+ **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\`.
671
+
672
+ **Env layering**:
673
+ 1. Root \`.env\` \u2014 \`DB_PROVIDER\`
674
+ 2. \`apps/api/.env\` \u2014 gateway transport endpoints
675
+ 3. \`apps/microservices/<name>/.env\` \u2014 each MS provider + transport
676
+ 4. \`apps/client/.env\` \u2014 \`VITE_API_URL\`
677
+
678
+ ## Commands
679
+
680
+ \`\`\`bash
681
+ ${devCmd} # start all services
682
+ ${nx} run api:serve # gateway only
683
+ ${nx} run auth:serve # auth MS only
684
+ ${nx} test <project> # run tests
685
+ ${nx} lint <project> # lint
686
+ ${nx} build <project> # build
687
+ ${nx} g @nx/nest:resource # generate NestJS resource
688
+ \`\`\`
689
+
690
+ ## .env files to configure
691
+
692
+ | File | Key vars |
693
+ |------|----------|
694
+ | \`apps/microservices/auth/.env\` | \`AUTH_PROVIDER=${opts.authProvider}\`, ${opts.authProvider === "supabase" ? "`SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`" : "`FB_ADMIN_*`, `FIREBASE_WEB_API_KEY`"} |
695
+ ${opts.upload !== "none" ? `| \`apps/microservices/upload/.env\` | \`STORAGE_PROVIDER=${opts.upload}\`, provider creds |
696
+ ` : ""}| \`apps/microservices/notes/.env\` | \`DB_PROVIDER=${opts.dbProvider}\`, DB creds |
697
+ | \`apps/client/.env\` | \`VITE_API_URL=/api\` (proxied to :3001 in dev) |
698
+
699
+ ## Testing
700
+
701
+ - Unit tests: Vitest, files named \`*.unit.test.ts(x)\` in \`__tests__/\` next to source.
702
+ - Test behaviour, not implementation. Fake strategies from \`@icore/shared\` (FakeAuthStrategy etc.) serve as test doubles.
703
+ - Run: \`${nx} test <project>\`
704
+ `;
705
+ await writeFile(join(targetDir, "AGENTS.md"), agents);
706
+ await mkdir(join(targetDir, ".claude"), { recursive: true });
707
+ const mcpServers = {
708
+ nx: {
709
+ command: "npx",
710
+ args: ["-y", "@nx/mcp@latest", "--directory", "."],
711
+ type: "stdio"
712
+ }
713
+ };
714
+ if (usesSupabase) {
715
+ mcpServers["supabase"] = {
716
+ command: "npx",
717
+ args: [
718
+ "-y",
719
+ "@supabase/mcp-server-supabase@latest",
720
+ "--access-token",
721
+ "<SUPABASE_PERSONAL_ACCESS_TOKEN>"
722
+ ],
723
+ type: "stdio"
724
+ };
725
+ }
726
+ if (usesFirebase) {
727
+ mcpServers["firebase"] = {
728
+ command: "npx",
729
+ args: ["-y", "firebase-tools@latest", "experimental:mcp"],
730
+ type: "stdio"
731
+ };
732
+ }
733
+ const nxCmds = [`Bash(${nx} *)`, `Bash(${devCmd})`];
734
+ if (pm !== "npm") nxCmds.push(`Bash(npx nx *)`);
735
+ const settings = {
736
+ mcpServers,
737
+ permissions: {
738
+ allow: [
739
+ ...nxCmds,
740
+ "Bash(npx prettier *)",
741
+ "Bash(git status)",
742
+ "Bash(git diff *)",
743
+ "Bash(git log *)"
744
+ ]
745
+ }
746
+ };
747
+ await writeFile(
748
+ join(targetDir, ".claude", "settings.json"),
749
+ JSON.stringify(settings, null, 2) + "\n"
750
+ );
751
+ }
482
752
 
483
753
  // src/lib/prompts.ts
484
754
  import * as p from "@clack/prompts";
@@ -674,6 +944,12 @@ Re-run with @latest to refresh:
674
944
  });
675
945
  if (p.isCancel(transport)) throw new Error("cancelled");
676
946
  const packageManager = flags.packageManager ?? detectPackageManager();
947
+ if (packageManager === "yarn") {
948
+ p.note(
949
+ "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]",
950
+ "\u26A0 yarn 24h age-gate"
951
+ );
952
+ }
677
953
  const initGit = flags.initGit ?? !await p.confirm({ message: "Initialise git repo?", initialValue: true }) === false;
678
954
  const install = flags.install ?? !await p.confirm({
679
955
  message: `Run ${packageManager} install?`,
@@ -697,5 +973,6 @@ Re-run with @latest to refresh:
697
973
  }
698
974
  export {
699
975
  collectOptions,
976
+ pmRun,
700
977
  scaffold
701
978
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idevconn/create-icore",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "Bootstrap a new project from the iCore scaffold (Nx + NestJS + React + Vite + shadcn/Tailwind, swappable auth + storage providers).",
5
5
  "license": "Apache-2.0",
6
6
  "author": "iDEVconn",
@@ -15,5 +15,19 @@ UPLOAD_PORT=4002
15
15
  # UPLOAD_REDIS_URL=redis://localhost:6379
16
16
  # UPLOAD_NATS_URL=nats://localhost:4222
17
17
 
18
+ # Notes MS transport — must match apps/microservices/notes/.env
19
+ NOTES_TRANSPORT=tcp
20
+ NOTES_HOST=127.0.0.1
21
+ NOTES_PORT=4004
22
+ # NOTES_REDIS_URL=redis://localhost:6379
23
+ # NOTES_NATS_URL=nats://localhost:4222
24
+
25
+ # Payment MS transport — must match apps/microservices/payment/.env
26
+ PAYMENT_TRANSPORT=tcp
27
+ PAYMENT_HOST=127.0.0.1
28
+ PAYMENT_PORT=4003
29
+ # PAYMENT_REDIS_URL=redis://localhost:6379
30
+ # PAYMENT_NATS_URL=nats://localhost:4222
31
+
18
32
  # Per-request multipart file size cap (KB). Default 5120 (5 MB) when unset.
19
33
  MAX_FILE_SIZE_KB=5120
@@ -1,3 +1,4 @@
1
+ import { join } from 'node:path';
1
2
  import { Module } from '@nestjs/common';
2
3
  import { ConfigModule } from '@nestjs/config';
3
4
  import { ThrottlerModule, seconds } from '@nestjs/throttler';
@@ -11,7 +12,10 @@ import { AdminModule } from './admin/admin.module';
11
12
 
12
13
  @Module({
13
14
  imports: [
14
- ConfigModule.forRoot({ isGlobal: true }),
15
+ ConfigModule.forRoot({
16
+ isGlobal: true,
17
+ envFilePath: [join(process.cwd(), 'apps/api/.env'), join(process.cwd(), '.env')],
18
+ }),
15
19
  ThrottlerModule.forRoot([{ name: 'auth-burst', ttl: seconds(60), limit: 10 }]),
16
20
  AuthModule,
17
21
  AbilitiesModule,
@@ -3,9 +3,17 @@ import { NestFactory } from '@nestjs/core';
3
3
  import { NestExpressApplication } from '@nestjs/platform-express';
4
4
  import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
5
5
  import cookieParser from 'cookie-parser';
6
+ import { formatGatewayBanner } from '@icore/shared';
6
7
  import { AppModule } from './app/app.module';
7
8
  import pkg from '@icore/package.json';
8
9
 
10
+ const GATEWAY_SERVICES = [
11
+ { name: 'auth', prefix: 'AUTH' },
12
+ { name: 'upload', prefix: 'UPLOAD' },
13
+ { name: 'notes', prefix: 'NOTES' },
14
+ { name: 'payment', prefix: 'PAYMENT' },
15
+ ];
16
+
9
17
  const DEFAULT_PORT = 3001;
10
18
 
11
19
  async function bootstrap() {
@@ -28,12 +36,10 @@ async function bootstrap() {
28
36
 
29
37
  bootstrap()
30
38
  .then(() => {
31
- const logger = new Logger('API-Bootstrap');
32
- logger.log(
33
- `API Bootstrap completed successfully: ${process.env.API_ORIGIN ?? 'http://localhost'}:${process.env.API_PORT ?? DEFAULT_PORT}/api`,
34
- );
35
- logger.log(
36
- `Swagger UI: ${process.env.API_ORIGIN ?? 'http://localhost'}:${process.env.API_PORT ?? DEFAULT_PORT}/api/docs`,
39
+ const origin = process.env.API_ORIGIN ?? 'http://localhost';
40
+ const port = Number(process.env.API_PORT ?? DEFAULT_PORT);
41
+ new Logger('API-Bootstrap').log(
42
+ formatGatewayBanner({ port, origin, services: GATEWAY_SERVICES }),
37
43
  );
38
44
  })
39
45
  .catch((err) => {
@@ -5,8 +5,8 @@
5
5
  "dependencies": {
6
6
  "@icore/auth-firebase": "*",
7
7
  "@icore/auth-supabase": "*",
8
+ "@icore/firebase-admin": "*",
8
9
  "@icore/shared": "*",
9
- "firebase-admin": "^13.0.0",
10
10
  "@nestjs/common": "^11.1.24",
11
11
  "@nestjs/config": "^4.0.4",
12
12
  "@nestjs/core": "^11.1.24",
@@ -50,7 +50,8 @@
50
50
  "dependsOn": ["build"],
51
51
  "options": {
52
52
  "buildTarget": "auth:build",
53
- "runBuildTargetDependencies": false
53
+ "runBuildTargetDependencies": false,
54
+ "port": 9230
54
55
  },
55
56
  "configurations": {
56
57
  "development": {