@bluealba/platform-cli 0.2.1 → 0.3.0-feature-platform-cli-222

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 (22) hide show
  1. package/dist/index.js +716 -153
  2. package/package.json +5 -5
  3. package/templates/bootstrap-service-template/package.json +2 -2
  4. package/templates/customization-ui-module-template/package.json +2 -2
  5. package/templates/customization-ui-module-template/src/root.component.tsx +2 -11
  6. package/templates/platform-init-template/{local → core/local}/core-docker-compose.yml +4 -4
  7. package/templates/platform-init-template/{local → core/local}/platform-docker-compose.yml +1 -1
  8. package/templates/react-ui-module-template/package.json +1 -1
  9. package/templates/customization-ui-module-template/src/components/ExpandedNavbarLogo.tsx +0 -62
  10. package/templates/customization-ui-module-template/src/components/ExtensionPoints/index.tsx +0 -28
  11. package/templates/customization-ui-module-template/src/components/Logo.tsx +0 -55
  12. package/templates/customization-ui-module-template/src/components/SplashLogo.tsx +0 -52
  13. package/templates/customization-ui-module-template/src/hooks/useDynamicStyleSheet.ts +0 -18
  14. package/templates/platform-init-template/local/docker-compose.yml +0 -3
  15. package/templates/platform-init-template/local/package.json +0 -18
  16. package/templates/platform-init-template/local/scripts/build.sh +0 -18
  17. package/templates/platform-init-template/local/scripts/install.sh +0 -18
  18. /package/templates/platform-init-template/{local → core/local}/.env.example +0 -0
  19. /package/templates/platform-init-template/{local → core/local}/environment/pae-nestjs-gateway-service.env +0 -0
  20. /package/templates/platform-init-template/{local → core/local}/nginx.conf +0 -0
  21. /package/templates/platform-init-template/{local → core/local}/ssl/cert.pem +0 -0
  22. /package/templates/platform-init-template/{local → core/local}/ssl/key.pem +0 -0
package/dist/index.js CHANGED
@@ -61,11 +61,10 @@ import React2 from "react";
61
61
  import { Box as Box3, Text as Text3 } from "ink";
62
62
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
63
63
  var ASCII_ART = [
64
- " \\ | / ",
65
- " \\ | / ",
66
- " ----*---- ",
67
- " / | \\ ",
68
- " / | \\ "
64
+ "|---. .---|",
65
+ "| \\/ |",
66
+ "| /\\ |",
67
+ "|---' '---|"
69
68
  ];
70
69
  var WelcomeBanner = React2.memo(function WelcomeBanner2({ version: version2 }) {
71
70
  return /* @__PURE__ */ jsxs3(Box3, { borderStyle: "round", flexDirection: "column", paddingX: 1, paddingY: 1, children: [
@@ -125,8 +124,7 @@ function Spinner({ label }) {
125
124
  import { Fzf } from "fzf";
126
125
 
127
126
  // src/commands/create-application/create-application.command.ts
128
- import { join as join10 } from "path";
129
- import { cwd as cwd2 } from "process";
127
+ import { join as join10, resolve } from "path";
130
128
  import { mkdir as mkdir2 } from "fs/promises";
131
129
 
132
130
  // src/commands/create-application/scaffold-application-monorepo.ts
@@ -358,10 +356,10 @@ function buildUiModuleEntry(organizationName, applicationName, applicationDispla
358
356
 
359
357
  // src/commands/create-application/create-docker-compose.ts
360
358
  import { writeFile as writeFile3 } from "fs/promises";
361
- function buildBootstrapBlock(applicationName) {
359
+ function buildBootstrapBlock(platformName, applicationName) {
362
360
  return ` ${applicationName}-bootstrap-service:
363
361
  build:
364
- context: ../${applicationName}/services/${applicationName}-bootstrap-service
362
+ context: ../../${platformName}-${applicationName}/services/${applicationName}-bootstrap-service
365
363
  dockerfile: Dockerfile.development
366
364
  environment:
367
365
  - NODE_TLS_REJECT_UNAUTHORIZED=0
@@ -371,27 +369,27 @@ function buildBootstrapBlock(applicationName) {
371
369
  - GATEWAY_SERVICE_URL=\${PAE_GATEWAY_URL}
372
370
  - SERVICE_ACCESS_SECRET=\${PAE_GATEWAY_SERVICE_ACCESS_SECRET}
373
371
  volumes:
374
- - \${PWD}/../:/app/out
372
+ - \${PWD}/:/app/out
375
373
  depends_on:
376
374
  pae-nestjs-gateway-service:
377
375
  condition: service_healthy
378
376
  `;
379
377
  }
380
- function buildUiBlock(applicationName, uiPort) {
378
+ function buildUiBlock(platformName, applicationName, uiPort) {
381
379
  return ` ${applicationName}-ui:
382
380
  build:
383
- context: ../${applicationName}/ui/${applicationName}-ui
381
+ context: ../../${platformName}-${applicationName}/ui/${applicationName}-ui
384
382
  dockerfile: Dockerfile.development
385
383
  ports:
386
384
  - ${uiPort}:80
387
385
  volumes:
388
- - \${PWD}/../:/app/out
386
+ - \${PWD}/:/app/out
389
387
  `;
390
388
  }
391
- function buildBackendBlock(applicationName, servicePort) {
389
+ function buildBackendBlock(platformName, applicationName, servicePort) {
392
390
  return ` ${applicationName}-service:
393
391
  build:
394
- context: ../${applicationName}/services/${applicationName}-service
392
+ context: ../../${platformName}-${applicationName}/services/${applicationName}-service
395
393
  dockerfile: Dockerfile.development
396
394
  args:
397
395
  - BA_NPM_AUTH_TOKEN=$BA_NPM_AUTH_TOKEN
@@ -400,18 +398,18 @@ function buildBackendBlock(applicationName, servicePort) {
400
398
  environment:
401
399
  - GATEWAY_URL=\${PAE_GATEWAY_URL}
402
400
  volumes:
403
- - \${PWD}/../:/app/out
401
+ - \${PWD}/:/app/out
404
402
  `;
405
403
  }
406
- async function createDockerCompose(dockerComposePath, applicationName, hasUserInterface, hasBackendService, uiPort, servicePort, logger) {
407
- const blocks = ["services:\n", buildBootstrapBlock(applicationName)];
404
+ async function createDockerCompose(dockerComposePath, applicationName, platformName, hasUserInterface, hasBackendService, uiPort, servicePort, logger) {
405
+ const blocks = ["services:\n", buildBootstrapBlock(platformName, applicationName)];
408
406
  if (hasUserInterface) {
409
- blocks.push(buildUiBlock(applicationName, uiPort));
407
+ blocks.push(buildUiBlock(platformName, applicationName, uiPort));
410
408
  }
411
409
  if (hasBackendService) {
412
- blocks.push(buildBackendBlock(applicationName, servicePort));
410
+ blocks.push(buildBackendBlock(platformName, applicationName, servicePort));
413
411
  }
414
- const content = blocks.join("\n");
412
+ const content = blocks.join("\n") + "\n";
415
413
  try {
416
414
  await writeFile3(dockerComposePath, content, "utf-8");
417
415
  logger.log(`Created docker-compose: ${dockerComposePath}`);
@@ -421,32 +419,18 @@ async function createDockerCompose(dockerComposePath, applicationName, hasUserIn
421
419
  }
422
420
  }
423
421
 
424
- // src/commands/create-application/update-root-docker-compose.ts
425
- import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
426
- async function updateRootDockerCompose(rootDockerComposePath, applicationName, logger) {
427
- try {
428
- const existing = await readFile3(rootDockerComposePath, "utf-8");
429
- const includeLine = ` - path: ./${applicationName}-docker-compose.yml
430
- `;
431
- await writeFile4(rootDockerComposePath, existing + includeLine, "utf-8");
432
- logger.log(`Updated root docker-compose: ${rootDockerComposePath}`);
433
- } catch (err) {
434
- const message = err instanceof Error ? err.message : String(err);
435
- logger.log(`Warning: Could not update root docker-compose \u2014 ${message}`);
436
- }
437
- }
438
-
439
422
  // src/commands/create-application/port-allocator.ts
440
423
  import { join as join8 } from "path";
441
- import { readdir as readdir2, readFile as readFile4 } from "fs/promises";
442
- async function getNextAvailablePort(localDir) {
424
+ import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
425
+ async function getNextAvailablePort(rootDir) {
443
426
  const allPorts = [];
427
+ const localDir = join8(rootDir, "core", "local");
444
428
  try {
445
429
  const files = await readdir2(localDir);
446
- const dockerComposeFiles = files.filter((f) => f.endsWith("-docker-compose.yml"));
447
- for (const file of dockerComposeFiles) {
430
+ const ymlFiles = files.filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
431
+ for (const file of ymlFiles) {
448
432
  try {
449
- const content = await readFile4(join8(localDir, file), "utf-8");
433
+ const content = await readFile3(join8(localDir, file), "utf-8");
450
434
  const regex = /^\s*-\s*"?(\d+):\d+"?\s*$/gm;
451
435
  let match;
452
436
  while ((match = regex.exec(content)) !== null) {
@@ -466,18 +450,69 @@ function formatError(err) {
466
450
  }
467
451
 
468
452
  // src/utils/platform-check.ts
469
- import { access } from "fs/promises";
453
+ import { cwd as cwd2 } from "process";
454
+ import { dirname as dirname7 } from "path";
455
+
456
+ // src/utils/manifest.ts
457
+ import { readFile as readFile4, writeFile as writeFile4, access } from "fs/promises";
470
458
  import { join as join9 } from "path";
471
459
  import { cwd } from "process";
472
- async function isPlatformInitialized() {
460
+ var MANIFEST_RELATIVE_PATH = join9("core", "product.manifest.json");
461
+ function manifestPath(rootDir) {
462
+ return join9(rootDir, MANIFEST_RELATIVE_PATH);
463
+ }
464
+ async function readManifest(rootDir = cwd()) {
465
+ const content = await readFile4(manifestPath(rootDir), "utf-8");
466
+ return JSON.parse(content);
467
+ }
468
+ async function writeManifest(manifest, rootDir = cwd()) {
469
+ await writeFile4(manifestPath(rootDir), JSON.stringify(manifest, null, 2), "utf-8");
470
+ }
471
+ async function manifestExists(rootDir = cwd()) {
473
472
  try {
474
- await access(join9(cwd(), "core"));
475
- await access(join9(cwd(), "local"));
473
+ await access(manifestPath(rootDir));
476
474
  return true;
477
475
  } catch {
478
476
  return false;
479
477
  }
480
478
  }
479
+ function createInitialManifest(params) {
480
+ const { organizationName, platformName, platformDisplayName } = params;
481
+ return {
482
+ version: "1",
483
+ product: {
484
+ name: platformName,
485
+ displayName: platformDisplayName,
486
+ organization: organizationName,
487
+ scope: `@${organizationName}`
488
+ },
489
+ applications: []
490
+ };
491
+ }
492
+ function addApplicationToManifest(manifest, app) {
493
+ return {
494
+ ...manifest,
495
+ applications: [...manifest.applications, app]
496
+ };
497
+ }
498
+
499
+ // src/utils/platform-check.ts
500
+ async function findRootDir(startDir = cwd2()) {
501
+ let dir = startDir;
502
+ while (true) {
503
+ if (await manifestExists(dir)) {
504
+ return dir;
505
+ }
506
+ const parent = dirname7(dir);
507
+ if (parent === dir) {
508
+ return null;
509
+ }
510
+ dir = parent;
511
+ }
512
+ }
513
+ async function isPlatformInitialized() {
514
+ return await findRootDir() !== null;
515
+ }
481
516
 
482
517
  // src/commands/create-application/create-application.command.ts
483
518
  var CREATE_APPLICATION_COMMAND_NAME = "create-application";
@@ -487,22 +522,28 @@ var createApplicationCommand = {
487
522
  };
488
523
  async function createApplication(params, logger) {
489
524
  const {
490
- organizationName,
491
- platformName,
492
525
  applicationName,
493
526
  applicationDisplayName,
494
527
  applicationDescription,
495
528
  hasUserInterface,
496
529
  hasBackendService
497
530
  } = params;
498
- if (!await isPlatformInitialized()) {
531
+ const rootDir = await findRootDir();
532
+ if (!rootDir) {
499
533
  logger.log("Error: Cannot create an application \u2014 no platform initialized in this directory.");
500
534
  return;
501
535
  }
502
- const rootDir = cwd2();
503
- const applicationDir = join10(rootDir, applicationName);
536
+ let manifest;
537
+ try {
538
+ manifest = await readManifest(rootDir);
539
+ } catch (err) {
540
+ logger.log(`Error: Could not read product manifest \u2014 ${formatError(err)}`);
541
+ return;
542
+ }
543
+ const { organization: organizationName, name: platformName } = manifest.product;
544
+ const localPath = `../${platformName}-${applicationName}`;
545
+ const applicationDir = resolve(join10(rootDir, "core"), localPath);
504
546
  const bootstrapServiceDir = join10(applicationDir, "services", `${applicationName}-bootstrap-service`);
505
- const localDir = join10(rootDir, "local");
506
547
  logger.log(`Creating application monorepo "${applicationName}"...`);
507
548
  try {
508
549
  await scaffoldApplicationMonorepo(applicationDir, organizationName, platformName, applicationName, logger);
@@ -513,7 +554,7 @@ async function createApplication(params, logger) {
513
554
  await mkdir2(join10(applicationDir, "services"), { recursive: true });
514
555
  await mkdir2(join10(applicationDir, "ui"), { recursive: true });
515
556
  logger.log(`Creating bootstrap service "${applicationName}-bootstrap-service"...`);
516
- const bootstrapServiceDir_var = `${applicationName}/services`;
557
+ const bootstrapServiceDir_var = `${platformName}-${applicationName}/services`;
517
558
  try {
518
559
  await scaffoldBootstrap(bootstrapServiceDir, organizationName, applicationName, bootstrapServiceDir_var, logger);
519
560
  } catch (err) {
@@ -545,7 +586,7 @@ async function createApplication(params, logger) {
545
586
  }
546
587
  if (hasUserInterface) {
547
588
  const uiDir = join10(applicationDir, "ui", `${applicationName}-ui`);
548
- const uiBaseDir = `${applicationName}/ui`;
589
+ const uiBaseDir = `${platformName}-${applicationName}/ui`;
549
590
  try {
550
591
  await scaffoldUiModule(uiDir, organizationName, platformName, applicationName, applicationDisplayName, uiBaseDir, logger);
551
592
  } catch (err) {
@@ -555,7 +596,7 @@ async function createApplication(params, logger) {
555
596
  }
556
597
  if (hasBackendService) {
557
598
  const serviceDir = join10(applicationDir, "services", `${applicationName}-service`);
558
- const serviceBaseDir = `${applicationName}/services`;
599
+ const serviceBaseDir = `${platformName}-${applicationName}/services`;
559
600
  try {
560
601
  await scaffoldNestjsService(serviceDir, organizationName, applicationName, applicationDisplayName, serviceBaseDir, logger);
561
602
  } catch (err) {
@@ -563,21 +604,33 @@ async function createApplication(params, logger) {
563
604
  return;
564
605
  }
565
606
  }
566
- const dockerComposePath = join10(localDir, `${applicationName}-docker-compose.yml`);
567
- const basePort = await getNextAvailablePort(localDir);
607
+ const dockerComposePath = join10(rootDir, "core", "local", `${applicationName}-docker-compose.yml`);
608
+ const basePort = await getNextAvailablePort(rootDir);
568
609
  const uiPort = hasUserInterface ? basePort : 0;
569
610
  const servicePort = hasBackendService ? hasUserInterface ? basePort + 1 : basePort : 0;
570
611
  await createDockerCompose(
571
612
  dockerComposePath,
572
613
  applicationName,
614
+ platformName,
573
615
  hasUserInterface,
574
616
  hasBackendService,
575
617
  uiPort,
576
618
  servicePort,
577
619
  logger
578
620
  );
579
- const rootDockerComposePath = join10(localDir, "docker-compose.yml");
580
- await updateRootDockerCompose(rootDockerComposePath, applicationName, logger);
621
+ const updatedManifest = addApplicationToManifest(manifest, {
622
+ name: applicationName,
623
+ displayName: applicationDisplayName,
624
+ description: applicationDescription,
625
+ localPath,
626
+ repository: null
627
+ });
628
+ try {
629
+ await writeManifest(updatedManifest, rootDir);
630
+ logger.log(`Updated product manifest with application "${applicationName}".`);
631
+ } catch (err) {
632
+ logger.log(`Warning: Could not update product manifest \u2014 ${formatError(err)}`);
633
+ }
581
634
  logger.log(`Done! Application "${applicationName}" created at ${applicationDir}`);
582
635
  }
583
636
 
@@ -592,9 +645,9 @@ function camelize(name) {
592
645
 
593
646
  // src/commands/init/scaffold-platform.ts
594
647
  import { fileURLToPath as fileURLToPath6 } from "url";
595
- import { join as join11, dirname as dirname7 } from "path";
648
+ import { join as join11, dirname as dirname8 } from "path";
596
649
  var templateDir = join11(
597
- dirname7(fileURLToPath6(import.meta.url)),
650
+ dirname8(fileURLToPath6(import.meta.url)),
598
651
  "..",
599
652
  "templates",
600
653
  "platform-init-template"
@@ -605,9 +658,9 @@ async function scaffoldPlatform(outputDir, variables, logger) {
605
658
 
606
659
  // src/commands/init/scaffold-platform-bootstrap.ts
607
660
  import { fileURLToPath as fileURLToPath7 } from "url";
608
- import { join as join12, dirname as dirname8 } from "path";
661
+ import { join as join12, dirname as dirname9 } from "path";
609
662
  var templateDir2 = join12(
610
- dirname8(fileURLToPath7(import.meta.url)),
663
+ dirname9(fileURLToPath7(import.meta.url)),
611
664
  "..",
612
665
  "templates",
613
666
  "bootstrap-service-template"
@@ -618,9 +671,9 @@ async function scaffoldPlatformBootstrap(outputDir, variables, logger) {
618
671
 
619
672
  // src/commands/init/scaffold-customization-ui.ts
620
673
  import { fileURLToPath as fileURLToPath8 } from "url";
621
- import { join as join13, dirname as dirname9 } from "path";
674
+ import { join as join13, dirname as dirname10 } from "path";
622
675
  var templateDir3 = join13(
623
- dirname9(fileURLToPath8(import.meta.url)),
676
+ dirname10(fileURLToPath8(import.meta.url)),
624
677
  "..",
625
678
  "templates",
626
679
  "customization-ui-module-template"
@@ -666,9 +719,9 @@ function generateRandomSecret() {
666
719
 
667
720
  // src/commands/init/generate-local-env.ts
668
721
  async function generateLocalEnv(outputDir, logger) {
669
- const examplePath = join15(outputDir, "local", ".env.example");
670
- const envPath = join15(outputDir, "local", ".env");
671
- logger.log("Generating local/.env with random secrets...");
722
+ const examplePath = join15(outputDir, "core", "local", ".env.example");
723
+ const envPath = join15(outputDir, "core", "local", ".env");
724
+ logger.log("Generating core/local/.env with random secrets...");
672
725
  const content = await readFile6(examplePath, "utf-8");
673
726
  const result = content.replace(/^(PAE_AUTH_JWT_SECRET|PAE_GATEWAY_SERVICE_ACCESS_SECRET|PAE_DB_PASSWORD)=$/gm, (_, key) => {
674
727
  return `${key}=${generateRandomSecret()}`;
@@ -708,7 +761,15 @@ async function init(params, logger) {
708
761
  try {
709
762
  await generateLocalEnv(outputDir, logger);
710
763
  } catch (err) {
711
- logger.log(`Error: Could not generate local/.env \u2014 ${formatError(err)}`);
764
+ logger.log(`Error: Could not generate core/local/.env \u2014 ${formatError(err)}`);
765
+ return;
766
+ }
767
+ try {
768
+ const manifest = createInitialManifest({ organizationName, platformName, platformDisplayName });
769
+ await writeManifest(manifest, outputDir);
770
+ logger.log("Created product manifest: core/product.manifest.json");
771
+ } catch (err) {
772
+ logger.log(`Error: Could not write product manifest \u2014 ${formatError(err)}`);
712
773
  return;
713
774
  }
714
775
  try {
@@ -746,7 +807,6 @@ async function init(params, logger) {
746
807
 
747
808
  // src/commands/configure-idp/configure-idp.command.ts
748
809
  import { join as join17 } from "path";
749
- import { cwd as cwd4 } from "process";
750
810
  import { fetch as undiciFetch, Agent } from "undici";
751
811
 
752
812
  // src/utils/env-reader.ts
@@ -824,11 +884,12 @@ var configureIdpCommand = {
824
884
  description: "Configure an Identity Provider (IDP) in the gateway"
825
885
  };
826
886
  async function configureIdp(params, logger) {
827
- if (!await isPlatformInitialized()) {
887
+ const rootDir = await findRootDir();
888
+ if (!rootDir) {
828
889
  logger.log("Error: Cannot configure an IDP \u2014 no platform initialized in this directory.");
829
890
  return;
830
891
  }
831
- const envPath = join17(cwd4(), "local", ".env");
892
+ const envPath = join17(rootDir, "core", "local", ".env");
832
893
  let env;
833
894
  try {
834
895
  env = await readEnvFile(envPath);
@@ -839,11 +900,11 @@ async function configureIdp(params, logger) {
839
900
  const gatewayUrl = env.get("PAE_GATEWAY_HOST_URL");
840
901
  const accessSecret = env.get("PAE_GATEWAY_SERVICE_ACCESS_SECRET");
841
902
  if (!gatewayUrl) {
842
- logger.log("Error: PAE_GATEWAY_HOST_URL is not set in local/.env");
903
+ logger.log("Error: PAE_GATEWAY_HOST_URL is not set in core/local/.env");
843
904
  return;
844
905
  }
845
906
  if (!accessSecret) {
846
- logger.log("Error: PAE_GATEWAY_SERVICE_ACCESS_SECRET is not set in local/.env");
907
+ logger.log("Error: PAE_GATEWAY_SERVICE_ACCESS_SECRET is not set in core/local/.env");
847
908
  return;
848
909
  }
849
910
  const provider = idpProviderRegistry.get(params.providerType);
@@ -879,6 +940,225 @@ async function configureIdp(params, logger) {
879
940
  logger.log(`IDP provider "${params.name}" configured successfully.`);
880
941
  }
881
942
 
943
+ // src/commands/create-service-module/create-service-module.command.ts
944
+ import { join as join19, resolve as resolve2 } from "path";
945
+ import { access as access2 } from "fs/promises";
946
+
947
+ // src/commands/create-service-module/scaffold-service-module.ts
948
+ import { fileURLToPath as fileURLToPath9 } from "url";
949
+ import { join as join18, dirname as dirname11 } from "path";
950
+ var nestjsServiceModuleTemplateDir2 = join18(
951
+ dirname11(fileURLToPath9(import.meta.url)),
952
+ "..",
953
+ "templates",
954
+ "nestjs-service-module-template"
955
+ );
956
+ async function scaffoldServiceModule(serviceDir, organizationName, serviceName, serviceDisplayName, serviceBaseDir, logger) {
957
+ logger.log(`Creating NestJS service "${serviceName}"...`);
958
+ await applyTemplate(
959
+ {
960
+ templateDir: nestjsServiceModuleTemplateDir2,
961
+ outputDir: serviceDir,
962
+ variables: { organizationName, serviceName, serviceDisplayName, serviceBaseDir }
963
+ },
964
+ (message) => logger.log(message)
965
+ );
966
+ logger.log(`Done! Service output: ${serviceDir}`);
967
+ }
968
+
969
+ // src/commands/create-service-module/service-module-entry-builder.ts
970
+ function buildCustomServiceModuleEntry(organizationName, serviceName, serviceDisplayName) {
971
+ return {
972
+ name: `@${organizationName}/${serviceName}`,
973
+ displayName: serviceDisplayName,
974
+ type: "service",
975
+ baseUrl: `/${serviceName}`,
976
+ service: {
977
+ host: serviceName,
978
+ port: 80
979
+ },
980
+ dependsOn: []
981
+ };
982
+ }
983
+
984
+ // src/commands/create-service-module/append-docker-compose.ts
985
+ import { readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
986
+ async function appendServiceToDockerCompose(dockerComposePath, serviceName, platformName, applicationName, port, logger) {
987
+ const block = `
988
+ ${serviceName}:
989
+ build:
990
+ context: ../../${platformName}-${applicationName}/services/${serviceName}
991
+ dockerfile: Dockerfile.development
992
+ args:
993
+ - BA_NPM_AUTH_TOKEN=$BA_NPM_AUTH_TOKEN
994
+ ports:
995
+ - ${port}:80
996
+ environment:
997
+ - GATEWAY_URL=\${PAE_GATEWAY_URL}
998
+ volumes:
999
+ - \${PWD}/:/app/out
1000
+ `;
1001
+ try {
1002
+ const existing = await readFile8(dockerComposePath, "utf-8");
1003
+ await writeFile7(dockerComposePath, existing + block, "utf-8");
1004
+ logger.log(`Updated docker-compose: ${dockerComposePath}`);
1005
+ } catch (err) {
1006
+ const message = err instanceof Error ? err.message : String(err);
1007
+ logger.log(`Warning: Could not update docker-compose \u2014 ${message}`);
1008
+ }
1009
+ }
1010
+
1011
+ // src/commands/create-service-module/create-service-module.command.ts
1012
+ var CREATE_SERVICE_MODULE_COMMAND_NAME = "create-service-module";
1013
+ var createServiceModuleCommand = {
1014
+ name: CREATE_SERVICE_MODULE_COMMAND_NAME,
1015
+ description: "Add a new service module to an existing application"
1016
+ };
1017
+ async function createServiceModule(params, logger) {
1018
+ const { applicationName, serviceName, serviceDisplayName } = params;
1019
+ const rootDir = await findRootDir();
1020
+ if (!rootDir) {
1021
+ logger.log("Error: Cannot create a service module \u2014 no platform initialized in this directory.");
1022
+ return;
1023
+ }
1024
+ let manifest;
1025
+ try {
1026
+ manifest = await readManifest(rootDir);
1027
+ } catch (err) {
1028
+ logger.log(`Error: Could not read product manifest \u2014 ${formatError(err)}`);
1029
+ return;
1030
+ }
1031
+ const { organization: organizationName, name: platformName } = manifest.product;
1032
+ const appEntry = manifest.applications.find((a) => a.name === applicationName);
1033
+ if (!appEntry) {
1034
+ logger.log(`Error: The specified application "${applicationName}" is not registered in the product manifest.`);
1035
+ return;
1036
+ }
1037
+ const applicationDir = resolve2(join19(rootDir, "core"), appEntry.localPath);
1038
+ try {
1039
+ await access2(applicationDir);
1040
+ } catch {
1041
+ logger.log(`Error: The specified application "${applicationName}" does not exist in the platform.`);
1042
+ return;
1043
+ }
1044
+ const fullServiceName = `${serviceName}-service`;
1045
+ const serviceDir = join19(applicationDir, "services", fullServiceName);
1046
+ try {
1047
+ await access2(serviceDir);
1048
+ logger.log(`Error: A service named "${fullServiceName}" already exists in application "${applicationName}".`);
1049
+ return;
1050
+ } catch {
1051
+ }
1052
+ const serviceBaseDir = `${platformName}-${applicationName}/services`;
1053
+ try {
1054
+ await scaffoldServiceModule(
1055
+ serviceDir,
1056
+ organizationName,
1057
+ fullServiceName,
1058
+ serviceDisplayName,
1059
+ serviceBaseDir,
1060
+ logger
1061
+ );
1062
+ } catch (err) {
1063
+ logger.log(`Error: Could not scaffold NestJS service \u2014 ${formatError(err)}`);
1064
+ return;
1065
+ }
1066
+ const bootstrapServiceDir = join19(applicationDir, "services", `${applicationName}-bootstrap-service`);
1067
+ const moduleEntry = buildCustomServiceModuleEntry(organizationName, fullServiceName, serviceDisplayName);
1068
+ await addModuleEntry(bootstrapServiceDir, applicationName, moduleEntry, logger);
1069
+ const dockerComposePath = join19(rootDir, "core", "local", `${applicationName}-docker-compose.yml`);
1070
+ const port = await getNextAvailablePort(rootDir);
1071
+ await appendServiceToDockerCompose(dockerComposePath, fullServiceName, platformName, applicationName, port, logger);
1072
+ logger.log(`Done! Service module "${fullServiceName}" added to application "${applicationName}".`);
1073
+ }
1074
+
1075
+ // src/commands/create-ui-module/create-ui-module.command.ts
1076
+ import { join as join20, resolve as resolve3 } from "path";
1077
+ import { access as access3, readdir as readdir3 } from "fs/promises";
1078
+
1079
+ // src/commands/create-ui-module/append-ui-docker-compose.ts
1080
+ import { readFile as readFile9, writeFile as writeFile8 } from "fs/promises";
1081
+ async function appendUiToDockerCompose(dockerComposePath, platformName, applicationName, port, logger) {
1082
+ const block = `
1083
+ ${applicationName}-ui:
1084
+ build:
1085
+ context: ../../${platformName}-${applicationName}/ui/${applicationName}-ui
1086
+ dockerfile: Dockerfile.development
1087
+ ports:
1088
+ - ${port}:80
1089
+ volumes:
1090
+ - \${PWD}/:/app/out
1091
+ `;
1092
+ try {
1093
+ const existing = await readFile9(dockerComposePath, "utf-8");
1094
+ await writeFile8(dockerComposePath, existing + block, "utf-8");
1095
+ logger.log(`Updated docker-compose: ${dockerComposePath}`);
1096
+ } catch (err) {
1097
+ const message = err instanceof Error ? err.message : String(err);
1098
+ logger.log(`Warning: Could not update docker-compose \u2014 ${message}`);
1099
+ }
1100
+ }
1101
+
1102
+ // src/commands/create-ui-module/create-ui-module.command.ts
1103
+ var CREATE_UI_MODULE_COMMAND_NAME = "create-ui-module";
1104
+ var createUiModuleCommand = {
1105
+ name: CREATE_UI_MODULE_COMMAND_NAME,
1106
+ description: "Add a UI module to an existing application"
1107
+ };
1108
+ async function createUiModule(params, logger) {
1109
+ const { applicationName, applicationDisplayName } = params;
1110
+ const rootDir = await findRootDir();
1111
+ if (!rootDir) {
1112
+ logger.log("Error: Cannot create a UI module \u2014 no platform initialized in this directory.");
1113
+ return;
1114
+ }
1115
+ let manifest;
1116
+ try {
1117
+ manifest = await readManifest(rootDir);
1118
+ } catch (err) {
1119
+ logger.log(`Error: Could not read product manifest \u2014 ${formatError(err)}`);
1120
+ return;
1121
+ }
1122
+ const { organization: organizationName, name: platformName } = manifest.product;
1123
+ const appEntry = manifest.applications.find((a) => a.name === applicationName);
1124
+ if (!appEntry) {
1125
+ logger.log(`Error: The specified application "${applicationName}" is not registered in the product manifest.`);
1126
+ return;
1127
+ }
1128
+ const applicationDir = resolve3(join20(rootDir, "core"), appEntry.localPath);
1129
+ try {
1130
+ await access3(applicationDir);
1131
+ } catch {
1132
+ logger.log(`Error: The specified application "${applicationName}" does not exist in the platform.`);
1133
+ return;
1134
+ }
1135
+ const uiDir = join20(applicationDir, "ui");
1136
+ try {
1137
+ const uiEntries = await readdir3(uiDir);
1138
+ const existingUiModules = uiEntries.filter((e) => e !== ".gitkeep");
1139
+ if (existingUiModules.length > 0) {
1140
+ logger.log(`Error: Currently we support only one UI module per application. Application "${applicationName}" already has a UI module.`);
1141
+ return;
1142
+ }
1143
+ } catch {
1144
+ }
1145
+ const uiOutputDir = join20(uiDir, `${applicationName}-ui`);
1146
+ const uiBaseDir = `${platformName}-${applicationName}/ui`;
1147
+ try {
1148
+ await scaffoldUiModule(uiOutputDir, organizationName, platformName, applicationName, applicationDisplayName, uiBaseDir, logger);
1149
+ } catch (err) {
1150
+ logger.log(`Error: Could not scaffold UI module \u2014 ${formatError(err)}`);
1151
+ return;
1152
+ }
1153
+ const bootstrapServiceDir = join20(applicationDir, "services", `${applicationName}-bootstrap-service`);
1154
+ const moduleEntry = buildUiModuleEntry(organizationName, applicationName, applicationDisplayName);
1155
+ await addModuleEntry(bootstrapServiceDir, applicationName, moduleEntry, logger);
1156
+ const dockerComposePath = join20(rootDir, "core", "local", `${applicationName}-docker-compose.yml`);
1157
+ const port = await getNextAvailablePort(rootDir);
1158
+ await appendUiToDockerCompose(dockerComposePath, platformName, applicationName, port, logger);
1159
+ logger.log(`Done! UI module "${applicationName}-ui" added to application "${applicationName}".`);
1160
+ }
1161
+
882
1162
  // src/commands/registry.ts
883
1163
  var CommandRegistry = class {
884
1164
  commands = /* @__PURE__ */ new Map();
@@ -904,6 +1184,8 @@ var registry = new CommandRegistry();
904
1184
  registry.register(createApplicationCommand);
905
1185
  registry.register(initCommand);
906
1186
  registry.register(configureIdpCommand);
1187
+ registry.register(createServiceModuleCommand);
1188
+ registry.register(createUiModuleCommand);
907
1189
 
908
1190
  // src/app-state.ts
909
1191
  var APP_STATE = {
@@ -916,11 +1198,6 @@ var APP_STATE = {
916
1198
  // src/hooks/use-command-runner.ts
917
1199
  import { useState as useState2, useCallback, useRef } from "react";
918
1200
 
919
- // src/controllers/ui/create-application.ui-controller.ts
920
- import { readFile as readFile8 } from "fs/promises";
921
- import { join as join18 } from "path";
922
- import { cwd as cwd5 } from "process";
923
-
924
1201
  // src/services/create-application.service.ts
925
1202
  async function createApplicationService(params, logger) {
926
1203
  await createApplication(params, logger);
@@ -932,24 +1209,6 @@ async function createApplicationUiController(ctx) {
932
1209
  ctx.log("Error: Cannot create an application \u2014 no platform initialized in this directory.");
933
1210
  return;
934
1211
  }
935
- let organizationName;
936
- let platformName;
937
- try {
938
- const corePackageJson = JSON.parse(
939
- await readFile8(join18(cwd5(), "core", "package.json"), "utf-8")
940
- );
941
- const scopeMatch = corePackageJson.name.match(/^@([^/]+)\//);
942
- organizationName = scopeMatch?.[1];
943
- const rawName = corePackageJson.name.replace(/^@[^/]+\//, "").replace(/-core$/, "");
944
- platformName = rawName;
945
- if (!organizationName || !platformName) {
946
- throw new Error(`Could not parse organization/platform from package name: "${corePackageJson.name}"`);
947
- }
948
- } catch (err) {
949
- const message = err instanceof Error ? err.message : String(err);
950
- ctx.log(`Error: Could not read core/package.json \u2014 ${message}`);
951
- return;
952
- }
953
1212
  const applicationName = await ctx.prompt("Application name:");
954
1213
  if (!/^[a-z0-9-]+$/.test(applicationName)) {
955
1214
  ctx.log(`Error: Application name "${applicationName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
@@ -969,8 +1228,6 @@ async function createApplicationUiController(ctx) {
969
1228
  }
970
1229
  await createApplicationService(
971
1230
  {
972
- organizationName,
973
- platformName,
974
1231
  applicationName,
975
1232
  applicationDisplayName,
976
1233
  applicationDescription,
@@ -1032,11 +1289,64 @@ async function configureIdpUiController(ctx) {
1032
1289
  );
1033
1290
  }
1034
1291
 
1292
+ // src/services/create-service-module.service.ts
1293
+ async function createServiceModuleService(params, logger) {
1294
+ await createServiceModule(params, logger);
1295
+ }
1296
+
1297
+ // src/controllers/ui/create-service-module.ui-controller.ts
1298
+ async function createServiceModuleUiController(ctx) {
1299
+ if (!await isPlatformInitialized()) {
1300
+ ctx.log("Error: Cannot create a service module \u2014 no platform initialized in this directory.");
1301
+ return;
1302
+ }
1303
+ const applicationName = await ctx.prompt("Application name:");
1304
+ if (!/^[a-z0-9-]+$/.test(applicationName)) {
1305
+ ctx.log(`Error: Application name "${applicationName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
1306
+ return;
1307
+ }
1308
+ const serviceName = await ctx.prompt("Service name:");
1309
+ if (!/^[a-z0-9-]+$/.test(serviceName)) {
1310
+ ctx.log(`Error: Service name "${serviceName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
1311
+ return;
1312
+ }
1313
+ const serviceDisplayName = await ctx.prompt("Service display name:");
1314
+ await createServiceModuleService(
1315
+ { applicationName, serviceName, serviceDisplayName },
1316
+ ctx
1317
+ );
1318
+ }
1319
+
1320
+ // src/services/create-ui-module.service.ts
1321
+ async function createUiModuleService(params, logger) {
1322
+ await createUiModule(params, logger);
1323
+ }
1324
+
1325
+ // src/controllers/ui/create-ui-module.ui-controller.ts
1326
+ async function createUiModuleUiController(ctx) {
1327
+ if (!await isPlatformInitialized()) {
1328
+ ctx.log("Error: Cannot create a UI module \u2014 no platform initialized in this directory.");
1329
+ return;
1330
+ }
1331
+ const applicationName = await ctx.prompt("Application name:");
1332
+ if (!/^[a-z0-9-]+$/.test(applicationName)) {
1333
+ ctx.log(`Error: Application name "${applicationName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
1334
+ return;
1335
+ }
1336
+ const applicationDisplayName = await ctx.prompt("Application display name:");
1337
+ await createUiModuleService(
1338
+ { applicationName, applicationDisplayName },
1339
+ ctx
1340
+ );
1341
+ }
1342
+
1035
1343
  // src/controllers/ui/registry.ts
1036
1344
  var uiControllers = /* @__PURE__ */ new Map([
1037
1345
  [CREATE_APPLICATION_COMMAND_NAME, createApplicationUiController],
1038
1346
  [INIT_COMMAND_NAME, initUiController],
1039
- [CONFIGURE_IDP_COMMAND_NAME, configureIdpUiController]
1347
+ [CONFIGURE_IDP_COMMAND_NAME, configureIdpUiController],
1348
+ [CREATE_SERVICE_MODULE_COMMAND_NAME, createServiceModuleUiController],
1349
+ [CREATE_UI_MODULE_COMMAND_NAME, createUiModuleUiController]
1040
1350
  ]);
1041
1351
 
1042
1352
  // src/hooks/use-command-runner.ts
@@ -1051,7 +1361,7 @@ function useCommandRunner({ appendStaticItem, setState }) {
1051
1361
  abortControllerRef.current = null;
1052
1362
  promptResolveRef.current = null;
1053
1363
  }, []);
1054
- const runCommand = useCallback(
1364
+ const runCommand2 = useCallback(
1055
1365
  (cmd) => {
1056
1366
  const controller = new AbortController();
1057
1367
  abortControllerRef.current = controller;
@@ -1065,14 +1375,14 @@ function useCommandRunner({ appendStaticItem, setState }) {
1065
1375
  }
1066
1376
  },
1067
1377
  prompt(message) {
1068
- return new Promise((resolve, reject) => {
1378
+ return new Promise((resolve5, reject) => {
1069
1379
  if (controller.signal.aborted) {
1070
1380
  reject(new DOMException("Aborted", "AbortError"));
1071
1381
  return;
1072
1382
  }
1073
1383
  setPromptMessage(message);
1074
1384
  setPromptValue("");
1075
- promptResolveRef.current = resolve;
1385
+ promptResolveRef.current = resolve5;
1076
1386
  setState(APP_STATE.PROMPTING);
1077
1387
  controller.signal.addEventListener(
1078
1388
  "abort",
@@ -1104,15 +1414,15 @@ function useCommandRunner({ appendStaticItem, setState }) {
1104
1414
  );
1105
1415
  const handlePromptSubmit = useCallback(
1106
1416
  (value) => {
1107
- const resolve = promptResolveRef.current;
1417
+ const resolve5 = promptResolveRef.current;
1108
1418
  promptResolveRef.current = null;
1109
1419
  setState(APP_STATE.EXECUTING);
1110
- resolve?.(value);
1420
+ resolve5?.(value);
1111
1421
  },
1112
1422
  [setState]
1113
1423
  );
1114
1424
  return {
1115
- runCommand,
1425
+ runCommand: runCommand2,
1116
1426
  handlePromptSubmit,
1117
1427
  abortExecution,
1118
1428
  promptMessage,
@@ -1134,7 +1444,7 @@ function App() {
1134
1444
  const appendStaticItem = useCallback2((item) => {
1135
1445
  setStaticItems((prev) => [...prev, item]);
1136
1446
  }, []);
1137
- const { runCommand, handlePromptSubmit, abortExecution, promptMessage, promptValue, setPromptValue } = useCommandRunner({ appendStaticItem, setState });
1447
+ const { runCommand: runCommand2, handlePromptSubmit, abortExecution, promptMessage, promptValue, setPromptValue } = useCommandRunner({ appendStaticItem, setState });
1138
1448
  const query = inputValue.startsWith("/") ? inputValue.slice(1) : "";
1139
1449
  const filteredCommands = registry.search(query);
1140
1450
  useInput(
@@ -1166,7 +1476,7 @@ function App() {
1166
1476
  const cmd = filteredCommands[selectedIndex];
1167
1477
  if (cmd) {
1168
1478
  setInputValue("");
1169
- runCommand(cmd);
1479
+ runCommand2(cmd);
1170
1480
  }
1171
1481
  return;
1172
1482
  }
@@ -1202,9 +1512,9 @@ function App() {
1202
1512
  if (!value.startsWith("/")) return;
1203
1513
  const name = value.slice(1).trim();
1204
1514
  const cmd = registry.get(name);
1205
- if (cmd) runCommand(cmd);
1515
+ if (cmd) runCommand2(cmd);
1206
1516
  },
1207
- [runCommand]
1517
+ [runCommand2]
1208
1518
  );
1209
1519
  function renderActiveArea() {
1210
1520
  switch (state) {
@@ -1240,9 +1550,6 @@ function App() {
1240
1550
  }
1241
1551
 
1242
1552
  // src/controllers/cli/create-application.cli-controller.ts
1243
- import { readFile as readFile9 } from "fs/promises";
1244
- import { join as join19 } from "path";
1245
- import { cwd as cwd6 } from "process";
1246
1553
  async function createApplicationCliController(args2) {
1247
1554
  if (!await isPlatformInitialized()) {
1248
1555
  console.error("Error: Cannot create an application \u2014 no platform initialized in this directory.");
@@ -1263,28 +1570,8 @@ async function createApplicationCliController(args2) {
1263
1570
  console.error(`Error: Application name "${applicationName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
1264
1571
  process.exit(1);
1265
1572
  }
1266
- let organizationName;
1267
- let platformName;
1268
- try {
1269
- const corePackageJson = JSON.parse(
1270
- await readFile9(join19(cwd6(), "core", "package.json"), "utf-8")
1271
- );
1272
- const scopeMatch = corePackageJson.name.match(/^@([^/]+)\//);
1273
- organizationName = scopeMatch?.[1];
1274
- const rawName = corePackageJson.name.replace(/^@[^/]+\//, "").replace(/-core$/, "");
1275
- platformName = rawName;
1276
- if (!organizationName || !platformName) {
1277
- throw new Error(`Could not parse organization/platform from package name: "${corePackageJson.name}"`);
1278
- }
1279
- } catch (err) {
1280
- const message = err instanceof Error ? err.message : String(err);
1281
- console.error(`Error: Could not read core/package.json \u2014 ${message}`);
1282
- process.exit(1);
1283
- }
1284
1573
  await createApplicationService(
1285
1574
  {
1286
- organizationName,
1287
- platformName,
1288
1575
  applicationName,
1289
1576
  applicationDisplayName,
1290
1577
  applicationDescription,
@@ -1341,22 +1628,167 @@ async function configureIdpCliController(args2) {
1341
1628
  await configureIdpService({ providerType, name, issuer, clientId, clientSecret, extras }, logger);
1342
1629
  }
1343
1630
 
1344
- // src/utils/run-npm-script.ts
1631
+ // src/commands/local-scripts/docker-compose-orchestrator.ts
1345
1632
  import { spawn } from "child_process";
1346
- import { access as access2 } from "fs/promises";
1347
- import { join as join20 } from "path";
1348
- async function runNpmScript(scriptName, logger, signal) {
1349
- const localDir = join20(process.cwd(), "local");
1350
- try {
1351
- await access2(localDir);
1352
- } catch {
1353
- logger.log(`Error: "local/" directory not found. Run "platform init" first.`);
1354
- return;
1633
+ import { access as access4 } from "fs/promises";
1634
+ import { join as join21 } from "path";
1635
+ function runDockerCompose(args2, logger, rootDir, signal) {
1636
+ return new Promise((resolvePromise) => {
1637
+ const child = spawn("docker", ["compose", ...args2], {
1638
+ shell: false,
1639
+ stdio: ["ignore", "pipe", "pipe"],
1640
+ cwd: rootDir
1641
+ });
1642
+ const onAbort = () => {
1643
+ child.kill("SIGTERM");
1644
+ };
1645
+ if (signal) {
1646
+ if (signal.aborted) {
1647
+ child.kill("SIGTERM");
1648
+ resolvePromise();
1649
+ return;
1650
+ }
1651
+ signal.addEventListener("abort", onAbort, { once: true });
1652
+ }
1653
+ child.stdout.on("data", (data) => {
1654
+ for (const line of data.toString().split("\n")) {
1655
+ if (line.trim()) logger.log(line);
1656
+ }
1657
+ });
1658
+ child.stderr.on("data", (data) => {
1659
+ for (const line of data.toString().split("\n")) {
1660
+ if (line.trim()) logger.log(line);
1661
+ }
1662
+ });
1663
+ child.on("close", (code, sig) => {
1664
+ signal?.removeEventListener("abort", onAbort);
1665
+ if (sig === "SIGTERM" || signal?.aborted) {
1666
+ logger.log("Command cancelled.");
1667
+ } else if (code !== 0) {
1668
+ logger.log(`docker compose exited with code ${code}.`);
1669
+ }
1670
+ resolvePromise();
1671
+ });
1672
+ child.on("error", (err) => {
1673
+ signal?.removeEventListener("abort", onAbort);
1674
+ logger.log(`Failed to run docker compose: ${err.message}`);
1675
+ resolvePromise();
1676
+ });
1677
+ });
1678
+ }
1679
+ async function getAppComposePaths(rootDir, manifest) {
1680
+ const results = [];
1681
+ for (const app of manifest.applications) {
1682
+ const composePath = join21(rootDir, "core", "local", `${app.name}-docker-compose.yml`);
1683
+ try {
1684
+ await access4(composePath);
1685
+ results.push({ composePath, appName: app.name });
1686
+ } catch {
1687
+ }
1688
+ }
1689
+ return results;
1690
+ }
1691
+ async function buildAllComposeArgs(rootDir, manifest, logger) {
1692
+ const localDir = join21(rootDir, "core", "local");
1693
+ const fileArgs = [
1694
+ "-f",
1695
+ join21(localDir, "platform-docker-compose.yml"),
1696
+ "-f",
1697
+ join21(localDir, "core-docker-compose.yml")
1698
+ ];
1699
+ const appEntries = await getAppComposePaths(rootDir, manifest);
1700
+ for (const { composePath, appName } of appEntries) {
1701
+ fileArgs.push("-f", composePath);
1702
+ }
1703
+ for (const app of manifest.applications) {
1704
+ if (!appEntries.find((e) => e.appName === app.name)) {
1705
+ logger.log(`Warning: No docker-compose found for application "${app.name}" in core/local/ \u2014 skipping.`);
1706
+ }
1355
1707
  }
1356
- return new Promise((resolve) => {
1357
- const child = spawn("npm", ["run", scriptName], {
1358
- cwd: localDir,
1359
- shell: true,
1708
+ return fileArgs;
1709
+ }
1710
+ async function startEnvironment(rootDir, manifest, logger, signal) {
1711
+ const platformName = manifest.product.name;
1712
+ const envFile = join21(rootDir, "core", "local", ".env");
1713
+ const fileArgs = await buildAllComposeArgs(rootDir, manifest, logger);
1714
+ const localDir = join21(rootDir, "core", "local");
1715
+ logger.log("Starting environment...");
1716
+ await runDockerCompose(
1717
+ [
1718
+ "-p",
1719
+ `${platformName}-platform`,
1720
+ "--project-directory",
1721
+ localDir,
1722
+ "--env-file",
1723
+ envFile,
1724
+ ...fileArgs,
1725
+ "up",
1726
+ "-d",
1727
+ "--build",
1728
+ "--remove-orphans"
1729
+ ],
1730
+ logger,
1731
+ rootDir,
1732
+ signal
1733
+ );
1734
+ }
1735
+ async function stopEnvironment(rootDir, manifest, logger, signal) {
1736
+ const platformName = manifest.product.name;
1737
+ const envFile = join21(rootDir, "core", "local", ".env");
1738
+ const fileArgs = await buildAllComposeArgs(rootDir, manifest, logger);
1739
+ const localDir = join21(rootDir, "core", "local");
1740
+ logger.log("Stopping environment...");
1741
+ await runDockerCompose(
1742
+ [
1743
+ "-p",
1744
+ `${platformName}-platform`,
1745
+ "--project-directory",
1746
+ localDir,
1747
+ "--env-file",
1748
+ envFile,
1749
+ ...fileArgs,
1750
+ "down"
1751
+ ],
1752
+ logger,
1753
+ rootDir,
1754
+ signal
1755
+ );
1756
+ }
1757
+ async function destroyEnvironment(rootDir, manifest, logger, signal) {
1758
+ const platformName = manifest.product.name;
1759
+ const envFile = join21(rootDir, "core", "local", ".env");
1760
+ const fileArgs = await buildAllComposeArgs(rootDir, manifest, logger);
1761
+ const localDir = join21(rootDir, "core", "local");
1762
+ logger.log("Destroying environment...");
1763
+ await runDockerCompose(
1764
+ [
1765
+ "-p",
1766
+ `${platformName}-platform`,
1767
+ "--project-directory",
1768
+ localDir,
1769
+ "--env-file",
1770
+ envFile,
1771
+ ...fileArgs,
1772
+ "down",
1773
+ "-v",
1774
+ "--rmi",
1775
+ "all"
1776
+ ],
1777
+ logger,
1778
+ rootDir,
1779
+ signal
1780
+ );
1781
+ }
1782
+
1783
+ // src/commands/local-scripts/npm-orchestrator.ts
1784
+ import { spawn as spawn2 } from "child_process";
1785
+ import { access as access5 } from "fs/promises";
1786
+ import { join as join22, resolve as resolve4 } from "path";
1787
+ function runCommand(command, args2, workDir, logger, signal) {
1788
+ return new Promise((resolvePromise) => {
1789
+ const child = spawn2(command, args2, {
1790
+ cwd: workDir,
1791
+ shell: false,
1360
1792
  stdio: ["ignore", "pipe", "pipe"]
1361
1793
  });
1362
1794
  const onAbort = () => {
@@ -1365,7 +1797,7 @@ async function runNpmScript(scriptName, logger, signal) {
1365
1797
  if (signal) {
1366
1798
  if (signal.aborted) {
1367
1799
  child.kill("SIGTERM");
1368
- resolve();
1800
+ resolvePromise();
1369
1801
  return;
1370
1802
  }
1371
1803
  signal.addEventListener("abort", onAbort, { once: true });
@@ -1383,19 +1815,72 @@ async function runNpmScript(scriptName, logger, signal) {
1383
1815
  child.on("close", (code, sig) => {
1384
1816
  signal?.removeEventListener("abort", onAbort);
1385
1817
  if (sig === "SIGTERM" || signal?.aborted) {
1386
- logger.log(`Command cancelled.`);
1818
+ logger.log("Command cancelled.");
1387
1819
  } else if (code !== 0) {
1388
- logger.log(`Command "npm run ${scriptName}" exited with code ${code}.`);
1820
+ logger.log(`Command "${command} ${args2.join(" ")}" exited with code ${code}.`);
1389
1821
  }
1390
- resolve();
1822
+ resolvePromise();
1391
1823
  });
1392
1824
  child.on("error", (err) => {
1393
1825
  signal?.removeEventListener("abort", onAbort);
1394
- logger.log(`Failed to run "npm run ${scriptName}": ${err.message}`);
1395
- resolve();
1826
+ logger.log(`Failed to run "${command} ${args2.join(" ")}": ${err.message}`);
1827
+ resolvePromise();
1396
1828
  });
1397
1829
  });
1398
1830
  }
1831
+ async function dirExists(dirPath) {
1832
+ try {
1833
+ await access5(dirPath);
1834
+ return true;
1835
+ } catch {
1836
+ return false;
1837
+ }
1838
+ }
1839
+ async function installDependencies(rootDir, manifest, logger, signal) {
1840
+ const coreDir = join22(rootDir, "core");
1841
+ const appDirs = [];
1842
+ for (const app of manifest.applications) {
1843
+ const appDir = resolve4(join22(rootDir, "core"), app.localPath);
1844
+ if (!await dirExists(appDir)) {
1845
+ logger.log(`Warning: Application directory "${app.name}" not found at ${appDir} \u2014 skipping install.`);
1846
+ continue;
1847
+ }
1848
+ appDirs.push({ name: app.name, dir: appDir });
1849
+ }
1850
+ const targets = [{ name: "core", dir: coreDir }, ...appDirs];
1851
+ logger.log(`Installing dependencies in parallel: ${targets.map((t) => t.name).join(", ")}...`);
1852
+ await Promise.all(
1853
+ targets.map(
1854
+ ({ name, dir }) => runCommand("npm", ["install"], dir, logger, signal).then(() => {
1855
+ logger.log(`\u2713 ${name} install done`);
1856
+ })
1857
+ )
1858
+ );
1859
+ }
1860
+ async function buildAll(rootDir, manifest, logger, signal) {
1861
+ const coreDir = join22(rootDir, "core");
1862
+ logger.log("Building core/...");
1863
+ await runCommand("npx", ["turbo", "build"], coreDir, logger, signal);
1864
+ if (signal?.aborted) return;
1865
+ const appDirs = [];
1866
+ for (const app of manifest.applications) {
1867
+ const appDir = resolve4(join22(rootDir, "core"), app.localPath);
1868
+ if (!await dirExists(appDir)) {
1869
+ logger.log(`Warning: Application directory "${app.name}" not found at ${appDir} \u2014 skipping build.`);
1870
+ continue;
1871
+ }
1872
+ appDirs.push({ name: app.name, dir: appDir });
1873
+ }
1874
+ if (appDirs.length === 0) return;
1875
+ logger.log(`Building apps in parallel: ${appDirs.map((a) => a.name).join(", ")}...`);
1876
+ await Promise.all(
1877
+ appDirs.map(
1878
+ ({ name, dir }) => runCommand("npm", ["run", "build"], dir, logger, signal).then(() => {
1879
+ logger.log(`\u2713 ${name} build done`);
1880
+ })
1881
+ )
1882
+ );
1883
+ }
1399
1884
 
1400
1885
  // src/commands/local-scripts/local-script.command.ts
1401
1886
  var INSTALL_COMMAND_NAME = "install";
@@ -1404,7 +1889,37 @@ var START_COMMAND_NAME = "start";
1404
1889
  var STOP_COMMAND_NAME = "stop";
1405
1890
  var DESTROY_COMMAND_NAME = "destroy";
1406
1891
  async function runLocalScript(scriptName, logger, signal) {
1407
- await runNpmScript(scriptName, logger, signal);
1892
+ const rootDir = await findRootDir();
1893
+ if (!rootDir) {
1894
+ logger.log(`Error: Cannot run "${scriptName}" \u2014 no platform initialized in this directory.`);
1895
+ return;
1896
+ }
1897
+ let manifest;
1898
+ try {
1899
+ manifest = await readManifest(rootDir);
1900
+ } catch (err) {
1901
+ logger.log(`Error: Could not read product manifest \u2014 ${formatError(err)}`);
1902
+ return;
1903
+ }
1904
+ switch (scriptName) {
1905
+ case START_COMMAND_NAME:
1906
+ await startEnvironment(rootDir, manifest, logger, signal);
1907
+ break;
1908
+ case STOP_COMMAND_NAME:
1909
+ await stopEnvironment(rootDir, manifest, logger, signal);
1910
+ break;
1911
+ case DESTROY_COMMAND_NAME:
1912
+ await destroyEnvironment(rootDir, manifest, logger, signal);
1913
+ break;
1914
+ case INSTALL_COMMAND_NAME:
1915
+ await installDependencies(rootDir, manifest, logger, signal);
1916
+ break;
1917
+ case BUILD_COMMAND_NAME:
1918
+ await buildAll(rootDir, manifest, logger, signal);
1919
+ break;
1920
+ default:
1921
+ logger.log(`Error: Unknown script "${scriptName}".`);
1922
+ }
1408
1923
  }
1409
1924
 
1410
1925
  // src/services/local-script.service.ts
@@ -1455,11 +1970,59 @@ function createLocalScriptCliController(scriptName) {
1455
1970
  };
1456
1971
  }
1457
1972
 
1973
+ // src/controllers/cli/create-service-module.cli-controller.ts
1974
+ async function createServiceModuleCliController(args2) {
1975
+ if (!await isPlatformInitialized()) {
1976
+ console.error("Error: Cannot create a service module \u2014 no platform initialized in this directory.");
1977
+ process.exit(1);
1978
+ }
1979
+ const { applicationName, serviceName, serviceDisplayName } = args2;
1980
+ if (!applicationName || !serviceName || !serviceDisplayName) {
1981
+ console.error("Error: applicationName, serviceName, and serviceDisplayName are required.");
1982
+ process.exit(1);
1983
+ }
1984
+ if (!/^[a-z0-9-]+$/.test(applicationName)) {
1985
+ console.error(`Error: Application name "${applicationName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
1986
+ process.exit(1);
1987
+ }
1988
+ if (!/^[a-z0-9-]+$/.test(serviceName)) {
1989
+ console.error(`Error: Service name "${serviceName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
1990
+ process.exit(1);
1991
+ }
1992
+ await createServiceModuleService(
1993
+ { applicationName, serviceName, serviceDisplayName },
1994
+ { log: console.log }
1995
+ );
1996
+ }
1997
+
1998
+ // src/controllers/cli/create-ui-module.cli-controller.ts
1999
+ async function createUiModuleCliController(args2) {
2000
+ if (!await isPlatformInitialized()) {
2001
+ console.error("Error: Cannot create a UI module \u2014 no platform initialized in this directory.");
2002
+ process.exit(1);
2003
+ }
2004
+ const { applicationName, applicationDisplayName } = args2;
2005
+ if (!applicationName || !applicationDisplayName) {
2006
+ console.error("Error: applicationName and applicationDisplayName are required.");
2007
+ process.exit(1);
2008
+ }
2009
+ if (!/^[a-z0-9-]+$/.test(applicationName)) {
2010
+ console.error(`Error: Application name "${applicationName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
2011
+ process.exit(1);
2012
+ }
2013
+ await createUiModuleService(
2014
+ { applicationName, applicationDisplayName },
2015
+ { log: console.log }
2016
+ );
2017
+ }
2018
+
1458
2019
  // src/controllers/cli/registry.ts
1459
2020
  var cliControllers = /* @__PURE__ */ new Map([
1460
2021
  [CREATE_APPLICATION_COMMAND_NAME, createApplicationCliController],
1461
2022
  [INIT_COMMAND_NAME, initCliController],
1462
2023
  [CONFIGURE_IDP_COMMAND_NAME, configureIdpCliController],
2024
+ [CREATE_SERVICE_MODULE_COMMAND_NAME, createServiceModuleCliController],
2025
+ [CREATE_UI_MODULE_COMMAND_NAME, createUiModuleCliController],
1463
2026
  [INSTALL_COMMAND_NAME, createLocalScriptCliController(INSTALL_COMMAND_NAME)],
1464
2027
  [BUILD_COMMAND_NAME, createLocalScriptCliController(BUILD_COMMAND_NAME)],
1465
2028
  [START_COMMAND_NAME, createLocalScriptCliController(START_COMMAND_NAME)],