@bluealba/platform-cli 1.1.1-alpha.0 → 1.2.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -204,8 +204,9 @@ function ConfirmPrompt({ value }) {
204
204
  import { Fzf } from "fzf";
205
205
 
206
206
  // src/commands/create-application/create-application.command.ts
207
- import { join as join11, resolve } from "path";
208
- import { mkdir as mkdir2 } from "fs/promises";
207
+ import { join as join12, resolve as resolve2 } from "path";
208
+ import { cwd as cwd4 } from "process";
209
+ import { mkdir as mkdir3 } from "fs/promises";
209
210
 
210
211
  // src/commands/create-application/scaffold-application-monorepo.ts
211
212
  import { fileURLToPath } from "url";
@@ -454,7 +455,7 @@ function buildBootstrapBlock(platformName, applicationName) {
454
455
  const bootstrapName = `${platformName}-${applicationName}-bootstrap-service`;
455
456
  return ` ${bootstrapName}:
456
457
  build:
457
- context: ../../${platformName}-${applicationName}/services/${bootstrapName}
458
+ context: ./services/${bootstrapName}
458
459
  dockerfile: Dockerfile.development
459
460
  environment:
460
461
  - NODE_TLS_REJECT_UNAUTHORIZED=0
@@ -473,7 +474,7 @@ function buildUiBlock(platformName, applicationName, uiPort) {
473
474
  const uiName = `${platformName}-${applicationName}-ui`;
474
475
  return ` ${uiName}:
475
476
  build:
476
- context: ../../${platformName}-${applicationName}/ui/${uiName}
477
+ context: ./ui/${uiName}
477
478
  dockerfile: Dockerfile.development
478
479
  ports:
479
480
  - ${uiPort}:80
@@ -485,7 +486,7 @@ function buildBackendBlock(platformName, applicationName, servicePort) {
485
486
  const serviceName = `${platformName}-${applicationName}-service`;
486
487
  return ` ${serviceName}:
487
488
  build:
488
- context: ../../${platformName}-${applicationName}/services/${serviceName}
489
+ context: ./services/${serviceName}
489
490
  dockerfile: Dockerfile.development
490
491
  ports:
491
492
  - ${servicePort}:80
@@ -514,25 +515,44 @@ async function createDockerCompose(dockerComposePath, applicationName, platformN
514
515
  }
515
516
 
516
517
  // src/commands/create-application/port-allocator.ts
517
- import { join as join8 } from "path";
518
- import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
519
- async function getNextAvailablePort(localDir) {
518
+ import { join as join8, resolve } from "path";
519
+ import { readdir as readdir2, readFile as readFile3, access } from "fs/promises";
520
+ async function extractPortsFromFile(filePath) {
521
+ try {
522
+ const content = await readFile3(filePath, "utf-8");
523
+ const regex = /^\s*-\s*"?(\d+):\d+"?\s*$/gm;
524
+ const ports = [];
525
+ let match;
526
+ while ((match = regex.exec(content)) !== null) {
527
+ ports.push(parseInt(match[1], 10));
528
+ }
529
+ return ports;
530
+ } catch {
531
+ return [];
532
+ }
533
+ }
534
+ async function getNextAvailablePort(localDir, manifest, coreDir) {
520
535
  const allPorts = [];
521
536
  try {
522
537
  const files = await readdir2(localDir);
523
538
  const ymlFiles = files.filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
524
539
  for (const file of ymlFiles) {
540
+ const ports = await extractPortsFromFile(join8(localDir, file));
541
+ allPorts.push(...ports);
542
+ }
543
+ } catch {
544
+ }
545
+ if (manifest && coreDir) {
546
+ for (const app of manifest.applications) {
547
+ const appDir = resolve(join8(coreDir), app.localPath);
548
+ const composePath = join8(appDir, "docker-compose.yml");
525
549
  try {
526
- const content = await readFile3(join8(localDir, file), "utf-8");
527
- const regex = /^\s*-\s*"?(\d+):\d+"?\s*$/gm;
528
- let match;
529
- while ((match = regex.exec(content)) !== null) {
530
- allPorts.push(parseInt(match[1], 10));
531
- }
550
+ await access(composePath);
551
+ const ports = await extractPortsFromFile(composePath);
552
+ allPorts.push(...ports);
532
553
  } catch {
533
554
  }
534
555
  }
535
- } catch {
536
556
  }
537
557
  return allPorts.length > 0 ? Math.max(...allPorts) + 1 : 9e3;
538
558
  }
@@ -543,7 +563,7 @@ function formatError(err) {
543
563
  }
544
564
 
545
565
  // src/utils/platform-layout.ts
546
- import { access, readdir as readdir3 } from "fs/promises";
566
+ import { access as access2, readdir as readdir3 } from "fs/promises";
547
567
  import { join as join9, dirname as dirname7 } from "path";
548
568
  import { cwd as cwd2 } from "process";
549
569
  async function findCoreDirIn(dir) {
@@ -557,7 +577,7 @@ async function findCoreDirIn(dir) {
557
577
  for (const entry of dirs) {
558
578
  if (entry.name.endsWith("-core")) {
559
579
  try {
560
- await access(join9(dir, entry.name, "product.manifest.json"));
580
+ await access2(join9(dir, entry.name, "product.manifest.json"));
561
581
  return entry.name;
562
582
  } catch {
563
583
  }
@@ -566,7 +586,7 @@ async function findCoreDirIn(dir) {
566
586
  const coreEntry = dirs.find((e) => e.name === "core");
567
587
  if (coreEntry) {
568
588
  try {
569
- await access(join9(dir, "core", "product.manifest.json"));
589
+ await access2(join9(dir, "core", "product.manifest.json"));
570
590
  return "core";
571
591
  } catch {
572
592
  }
@@ -600,7 +620,7 @@ async function isPlatformInitialized() {
600
620
  }
601
621
 
602
622
  // src/utils/manifest.ts
603
- import { readFile as readFile4, writeFile as writeFile4, access as access2 } from "fs/promises";
623
+ import { readFile as readFile4, writeFile as writeFile4, access as access3 } from "fs/promises";
604
624
  import { join as join10 } from "path";
605
625
  import { cwd as cwd3 } from "process";
606
626
  function manifestPath(rootDir, coreDirName = "core") {
@@ -633,12 +653,101 @@ function addApplicationToManifest(manifest, app) {
633
653
  };
634
654
  }
635
655
 
656
+ // src/commands/standalone/standalone-config.ts
657
+ import { readFile as readFile5, writeFile as writeFile5, mkdir as mkdir2, rename } from "fs/promises";
658
+ import { join as join11 } from "path";
659
+ import { homedir as homedir2 } from "os";
660
+ var CACHE_DIR = join11(homedir2(), ".ba-platform");
661
+ var CACHE_FILE = join11(CACHE_DIR, "standalone.json");
662
+ function getCachePath() {
663
+ return CACHE_FILE;
664
+ }
665
+ async function readCacheFile() {
666
+ try {
667
+ const content = await readFile5(CACHE_FILE, "utf-8");
668
+ const parsed = JSON.parse(content);
669
+ const defaults = parsed.defaults != null && typeof parsed.defaults === "object" ? parsed.defaults : {};
670
+ const apps = parsed.apps != null && typeof parsed.apps === "object" ? parsed.apps : {};
671
+ return { defaults, apps };
672
+ } catch (err) {
673
+ if (err instanceof SyntaxError) {
674
+ console.warn(`Warning: ~/.ba-platform/standalone.json is corrupt \u2014 starting with empty config.`);
675
+ }
676
+ return { defaults: {}, apps: {} };
677
+ }
678
+ }
679
+ async function writeCacheFile(cache) {
680
+ await mkdir2(CACHE_DIR, { recursive: true });
681
+ const tmpFile = CACHE_FILE + ".tmp";
682
+ await writeFile5(tmpFile, JSON.stringify(cache, null, 2), { encoding: "utf-8", mode: 384 });
683
+ await rename(tmpFile, CACHE_FILE);
684
+ }
685
+ function buildAppKey(platformName, applicationName) {
686
+ return `${platformName}/${applicationName}`;
687
+ }
688
+ async function resolveAppConfig(platformName, applicationName) {
689
+ const cache = await readCacheFile();
690
+ const key = buildAppKey(platformName, applicationName);
691
+ const app = cache.apps[key];
692
+ if (!app) return null;
693
+ return mergeConfig(cache.defaults, app);
694
+ }
695
+ function mergeConfig(defaults, app) {
696
+ return {
697
+ platformName: app.platformName ?? defaults.platformName ?? "",
698
+ organizationName: app.organizationName ?? defaults.organizationName ?? "",
699
+ applicationName: app.applicationName,
700
+ applicationDisplayName: app.applicationDisplayName,
701
+ idp: app.idp ?? defaults.idp,
702
+ adminUsers: app.adminUsers ?? defaults.adminUsers
703
+ };
704
+ }
705
+ async function saveDefaults(partial) {
706
+ const cache = await readCacheFile();
707
+ cache.defaults = { ...cache.defaults, ...partial };
708
+ await writeCacheFile(cache);
709
+ }
710
+ async function saveAppConfig(appKey, override) {
711
+ const cache = await readCacheFile();
712
+ cache.apps[appKey] = { ...cache.apps[appKey], ...override };
713
+ await writeCacheFile(cache);
714
+ }
715
+ async function deleteAppConfig(appKey) {
716
+ const cache = await readCacheFile();
717
+ if (!(appKey in cache.apps)) return false;
718
+ delete cache.apps[appKey];
719
+ await writeCacheFile(cache);
720
+ return true;
721
+ }
722
+ async function listAppConfigs() {
723
+ const cache = await readCacheFile();
724
+ return Object.entries(cache.apps).map(([key, app]) => ({
725
+ key,
726
+ resolved: mergeConfig(cache.defaults, app)
727
+ }));
728
+ }
729
+ async function getDefaults() {
730
+ const cache = await readCacheFile();
731
+ return cache.defaults;
732
+ }
733
+ var LEGACY_CONFIG_FILE = "standalone.json";
734
+ async function readLegacyConfig(appDir) {
735
+ try {
736
+ const content = await readFile5(join11(appDir, LEGACY_CONFIG_FILE), "utf-8");
737
+ const parsed = JSON.parse(content);
738
+ if (!parsed.platformName || !parsed.applicationName) return null;
739
+ return parsed;
740
+ } catch {
741
+ return null;
742
+ }
743
+ }
744
+
636
745
  // src/commands/create-application/create-application.command.ts
637
746
  var CREATE_APPLICATION_COMMAND_NAME = "create-application";
638
747
  var createApplicationCommand = {
639
748
  name: CREATE_APPLICATION_COMMAND_NAME,
640
- description: "Create an application in a platform",
641
- hidden: (ctx) => !ctx.platformInitialized
749
+ description: "Create an application",
750
+ hidden: () => false
642
751
  };
643
752
  async function createApplication(params, logger) {
644
753
  const {
@@ -649,23 +758,39 @@ async function createApplication(params, logger) {
649
758
  hasBackendService
650
759
  } = params;
651
760
  const layout = await findPlatformLayout();
652
- if (!layout) {
653
- logger.log("Error: Cannot create an application \u2014 no platform initialized in this directory.");
654
- return;
655
- }
656
- const { rootDir, coreDirName, localDir } = layout;
761
+ const insidePlatform = !!layout;
762
+ let platformName;
763
+ let organizationName;
764
+ let applicationDir;
765
+ let localDir;
766
+ let rootDir;
767
+ let coreDirName;
657
768
  let manifest;
658
- try {
659
- manifest = await readManifest(rootDir, coreDirName);
660
- } catch (err) {
661
- logger.log(`Error: Could not read product manifest \u2014 ${formatError(err)}`);
662
- return;
769
+ if (insidePlatform) {
770
+ rootDir = layout.rootDir;
771
+ coreDirName = layout.coreDirName;
772
+ localDir = layout.localDir;
773
+ try {
774
+ manifest = await readManifest(rootDir, coreDirName);
775
+ } catch (err) {
776
+ logger.log(`Error: Could not read product manifest \u2014 ${formatError(err)}`);
777
+ return;
778
+ }
779
+ platformName = manifest.product.name;
780
+ organizationName = manifest.product.organization;
781
+ const localPath = `../${platformName}-${applicationName}`;
782
+ applicationDir = resolve2(join12(rootDir, coreDirName), localPath);
783
+ } else {
784
+ if (!params.platformName || !params.organizationName) {
785
+ logger.log("Error: platformName and organizationName are required when creating an application outside a platform.");
786
+ return;
787
+ }
788
+ platformName = params.platformName;
789
+ organizationName = params.organizationName;
790
+ applicationDir = join12(cwd4(), `${platformName}-${applicationName}`);
663
791
  }
664
- const { organization: organizationName, name: platformName } = manifest.product;
665
- const localPath = `../${platformName}-${applicationName}`;
666
- const applicationDir = resolve(join11(rootDir, coreDirName), localPath);
667
792
  const bootstrapServiceName = `${platformName}-${applicationName}-bootstrap-service`;
668
- const bootstrapServiceDir = join11(applicationDir, "services", bootstrapServiceName);
793
+ const bootstrapServiceDir = join12(applicationDir, "services", bootstrapServiceName);
669
794
  logger.log(`Creating application monorepo "${applicationName}"...`);
670
795
  try {
671
796
  await scaffoldApplicationMonorepo(applicationDir, organizationName, platformName, applicationName, logger);
@@ -673,8 +798,8 @@ async function createApplication(params, logger) {
673
798
  logger.log(`Error: Could not scaffold application monorepo \u2014 ${formatError(err)}`);
674
799
  return;
675
800
  }
676
- await mkdir2(join11(applicationDir, "services"), { recursive: true });
677
- await mkdir2(join11(applicationDir, "ui"), { recursive: true });
801
+ await mkdir3(join12(applicationDir, "services"), { recursive: true });
802
+ await mkdir3(join12(applicationDir, "ui"), { recursive: true });
678
803
  logger.log(`Creating bootstrap service "${bootstrapServiceName}"...`);
679
804
  const bootstrapServiceBaseDir = `${platformName}-${applicationName}/services`;
680
805
  try {
@@ -708,7 +833,7 @@ async function createApplication(params, logger) {
708
833
  }
709
834
  if (hasUserInterface) {
710
835
  const uiName = `${platformName}-${applicationName}-ui`;
711
- const uiDir = join11(applicationDir, "ui", uiName);
836
+ const uiDir = join12(applicationDir, "ui", uiName);
712
837
  const uiBaseDir = `${platformName}-${applicationName}/ui`;
713
838
  try {
714
839
  await scaffoldUiModule(uiDir, organizationName, platformName, applicationName, applicationDisplayName, uiBaseDir, logger);
@@ -719,7 +844,7 @@ async function createApplication(params, logger) {
719
844
  }
720
845
  if (hasBackendService) {
721
846
  const serviceName = `${platformName}-${applicationName}-service`;
722
- const serviceDir = join11(applicationDir, "services", serviceName);
847
+ const serviceDir = join12(applicationDir, "services", serviceName);
723
848
  const serviceBaseDir = `${platformName}-${applicationName}/services`;
724
849
  try {
725
850
  await scaffoldNestjsService(serviceDir, organizationName, platformName, applicationName, applicationDisplayName, serviceBaseDir, logger);
@@ -728,10 +853,17 @@ async function createApplication(params, logger) {
728
853
  return;
729
854
  }
730
855
  }
731
- const dockerComposePath = join11(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
732
- const basePort = await getNextAvailablePort(localDir);
733
- const uiPort = hasUserInterface ? basePort : 0;
734
- const servicePort = hasBackendService ? hasUserInterface ? basePort + 1 : basePort : 0;
856
+ const dockerComposePath = join12(applicationDir, "docker-compose.yml");
857
+ let uiPort;
858
+ let servicePort;
859
+ if (insidePlatform && localDir && manifest) {
860
+ const basePort = await getNextAvailablePort(localDir, manifest, layout.coreDir);
861
+ uiPort = hasUserInterface ? basePort : 0;
862
+ servicePort = hasBackendService ? hasUserInterface ? basePort + 1 : basePort : 0;
863
+ } else {
864
+ uiPort = hasUserInterface ? 9e3 : 0;
865
+ servicePort = hasBackendService ? hasUserInterface ? 9001 : 9e3 : 0;
866
+ }
735
867
  await createDockerCompose(
736
868
  dockerComposePath,
737
869
  applicationName,
@@ -742,25 +874,40 @@ async function createApplication(params, logger) {
742
874
  servicePort,
743
875
  logger
744
876
  );
745
- const updatedManifest = addApplicationToManifest(manifest, {
746
- name: applicationName,
747
- displayName: applicationDisplayName,
748
- description: applicationDescription,
749
- localPath,
750
- repository: null
751
- });
752
- try {
753
- await writeManifest(updatedManifest, rootDir, coreDirName);
754
- logger.log(`Updated product manifest with application "${applicationName}".`);
755
- } catch (err) {
756
- logger.log(`Warning: Could not update product manifest \u2014 ${formatError(err)}`);
877
+ if (insidePlatform && manifest && rootDir && coreDirName) {
878
+ const localPath = `../${platformName}-${applicationName}`;
879
+ const updatedManifest = addApplicationToManifest(manifest, {
880
+ name: applicationName,
881
+ displayName: applicationDisplayName,
882
+ description: applicationDescription,
883
+ localPath,
884
+ repository: null
885
+ });
886
+ try {
887
+ await writeManifest(updatedManifest, rootDir, coreDirName);
888
+ logger.log(`Updated product manifest with application "${applicationName}".`);
889
+ } catch (err) {
890
+ logger.log(`Warning: Could not update product manifest \u2014 ${formatError(err)}`);
891
+ }
892
+ } else {
893
+ try {
894
+ await saveDefaults({ platformName, organizationName });
895
+ const appKey = buildAppKey(platformName, applicationName);
896
+ await saveAppConfig(appKey, { applicationName, applicationDisplayName });
897
+ logger.log(`Saved standalone config to ~/.ba-platform/standalone.json`);
898
+ } catch (err) {
899
+ logger.log(`Warning: Could not save standalone config \u2014 ${formatError(err)}`);
900
+ }
757
901
  }
758
902
  logger.log(`Done! Application "${applicationName}" created at ${applicationDir}`);
903
+ if (!insidePlatform) {
904
+ logger.log(`Next: cd ${platformName}-${applicationName} && platform standalone`);
905
+ }
759
906
  }
760
907
 
761
908
  // src/commands/init/init.command.ts
762
- import { join as join17 } from "path";
763
- import { cwd as cwd4 } from "process";
909
+ import { join as join18 } from "path";
910
+ import { cwd as cwd5 } from "process";
764
911
 
765
912
  // src/utils/string.ts
766
913
  function camelize(name) {
@@ -769,8 +916,8 @@ function camelize(name) {
769
916
 
770
917
  // src/commands/init/scaffold-platform.ts
771
918
  import { fileURLToPath as fileURLToPath6 } from "url";
772
- import { join as join12, dirname as dirname8 } from "path";
773
- var templateDir = join12(
919
+ import { join as join13, dirname as dirname8 } from "path";
920
+ var templateDir = join13(
774
921
  dirname8(fileURLToPath6(import.meta.url)),
775
922
  "..",
776
923
  "templates",
@@ -782,8 +929,8 @@ async function scaffoldPlatform(outputDir, variables, logger) {
782
929
 
783
930
  // src/commands/init/scaffold-platform-bootstrap.ts
784
931
  import { fileURLToPath as fileURLToPath7 } from "url";
785
- import { join as join13, dirname as dirname9 } from "path";
786
- var templateDir2 = join13(
932
+ import { join as join14, dirname as dirname9 } from "path";
933
+ var templateDir2 = join14(
787
934
  dirname9(fileURLToPath7(import.meta.url)),
788
935
  "..",
789
936
  "templates",
@@ -795,8 +942,8 @@ async function scaffoldPlatformBootstrap(outputDir, variables, logger) {
795
942
 
796
943
  // src/commands/init/scaffold-customization-ui.ts
797
944
  import { fileURLToPath as fileURLToPath8 } from "url";
798
- import { join as join14, dirname as dirname10 } from "path";
799
- var templateDir3 = join14(
945
+ import { join as join15, dirname as dirname10 } from "path";
946
+ var templateDir3 = join15(
800
947
  dirname10(fileURLToPath8(import.meta.url)),
801
948
  "..",
802
949
  "templates",
@@ -807,10 +954,10 @@ async function scaffoldCustomizationUi(outputDir, variables, logger) {
807
954
  }
808
955
 
809
956
  // src/commands/init/register-customization-module.ts
810
- import { join as join15 } from "path";
811
- import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
957
+ import { join as join16 } from "path";
958
+ import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
812
959
  async function registerCustomizationModule(bootstrapServiceDir, organizationName, logger, platformName = "platform") {
813
- const modulesJsonPath = join15(bootstrapServiceDir, "src", "data", "platform", "modules.json");
960
+ const modulesJsonPath = join16(bootstrapServiceDir, "src", "data", "platform", "modules.json");
814
961
  const customizationUiName = `${platformName}-customization-ui`;
815
962
  const entry = {
816
963
  name: `@${organizationName}/${customizationUiName}`,
@@ -826,15 +973,15 @@ async function registerCustomizationModule(bootstrapServiceDir, organizationName
826
973
  },
827
974
  dependsOn: []
828
975
  };
829
- const existing = JSON.parse(await readFile5(modulesJsonPath, "utf-8"));
976
+ const existing = JSON.parse(await readFile6(modulesJsonPath, "utf-8"));
830
977
  existing.push(entry);
831
- await writeFile5(modulesJsonPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
978
+ await writeFile6(modulesJsonPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
832
979
  logger.log(`Registered customization module in ${modulesJsonPath}`);
833
980
  }
834
981
 
835
982
  // src/commands/init/generate-local-env.ts
836
- import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
837
- import { join as join16 } from "path";
983
+ import { readFile as readFile7, writeFile as writeFile7 } from "fs/promises";
984
+ import { join as join17 } from "path";
838
985
 
839
986
  // src/utils/random.ts
840
987
  import { randomBytes } from "crypto";
@@ -844,14 +991,14 @@ function generateRandomSecret() {
844
991
 
845
992
  // src/commands/init/generate-local-env.ts
846
993
  async function generateLocalEnv(outputDir, logger, coreDirName = "core") {
847
- const examplePath = join16(outputDir, coreDirName, "local", ".env.example");
848
- const envPath = join16(outputDir, coreDirName, "local", ".env");
994
+ const examplePath = join17(outputDir, coreDirName, "local", ".env.example");
995
+ const envPath = join17(outputDir, coreDirName, "local", ".env");
849
996
  logger.log(`Generating ${coreDirName}/local/.env with random secrets...`);
850
- const content = await readFile6(examplePath, "utf-8");
997
+ const content = await readFile7(examplePath, "utf-8");
851
998
  const result = content.replace(/^(PAE_AUTH_JWT_SECRET|PAE_GATEWAY_SERVICE_ACCESS_SECRET|PAE_DB_PASSWORD)=$/gm, (_, key) => {
852
999
  return `${key}=${generateRandomSecret()}`;
853
1000
  });
854
- await writeFile6(envPath, result, "utf-8");
1001
+ await writeFile7(envPath, result, "utf-8");
855
1002
  }
856
1003
 
857
1004
  // src/commands/init/init.command.ts
@@ -868,7 +1015,7 @@ async function init(params, logger) {
868
1015
  }
869
1016
  const { organizationName, platformName, platformDisplayName } = params;
870
1017
  const platformTitle = camelize(platformName);
871
- const outputDir = cwd4();
1018
+ const outputDir = cwd5();
872
1019
  logger.log(`Initializing ${platformTitle} platform for @${organizationName}...`);
873
1020
  const coreDirName = `${platformName}-core`;
874
1021
  const bootstrapSuffix = params.bootstrapServiceSuffix ?? "bootstrap-service";
@@ -907,7 +1054,7 @@ async function init(params, logger) {
907
1054
  }
908
1055
  try {
909
1056
  await scaffoldPlatformBootstrap(
910
- join17(outputDir, coreDirName, "services", bootstrapServiceName),
1057
+ join18(outputDir, coreDirName, "services", bootstrapServiceName),
911
1058
  variables,
912
1059
  logger
913
1060
  );
@@ -917,7 +1064,7 @@ async function init(params, logger) {
917
1064
  }
918
1065
  try {
919
1066
  await scaffoldCustomizationUi(
920
- join17(outputDir, coreDirName, "ui", customizationUiName),
1067
+ join18(outputDir, coreDirName, "ui", customizationUiName),
921
1068
  variables,
922
1069
  logger
923
1070
  );
@@ -927,7 +1074,7 @@ async function init(params, logger) {
927
1074
  }
928
1075
  try {
929
1076
  await registerCustomizationModule(
930
- join17(outputDir, coreDirName, "services", bootstrapServiceName),
1077
+ join18(outputDir, coreDirName, "services", bootstrapServiceName),
931
1078
  organizationName,
932
1079
  logger,
933
1080
  platformName
@@ -940,15 +1087,15 @@ async function init(params, logger) {
940
1087
  }
941
1088
 
942
1089
  // src/commands/configure-idp/configure-idp.command.ts
943
- import { join as join18 } from "path";
1090
+ import { join as join19 } from "path";
944
1091
  import { fetch as undiciFetch, Agent } from "undici";
945
1092
 
946
1093
  // src/utils/env-reader.ts
947
- import { readFile as readFile7 } from "fs/promises";
1094
+ import { readFile as readFile8 } from "fs/promises";
948
1095
  async function readEnvFile(filePath) {
949
1096
  let content;
950
1097
  try {
951
- content = await readFile7(filePath, "utf-8");
1098
+ content = await readFile8(filePath, "utf-8");
952
1099
  } catch (error) {
953
1100
  if (error.code === "ENOENT") {
954
1101
  throw new Error(`.env file not found at ${filePath}`);
@@ -1024,7 +1171,7 @@ async function configureIdp(params, logger) {
1024
1171
  logger.log("Error: Cannot configure an IDP \u2014 no platform initialized in this directory.");
1025
1172
  return;
1026
1173
  }
1027
- const envPath = join18(layout.localDir, ".env");
1174
+ const envPath = join19(layout.localDir, ".env");
1028
1175
  let env;
1029
1176
  try {
1030
1177
  env = await readEnvFile(envPath);
@@ -1076,13 +1223,13 @@ async function configureIdp(params, logger) {
1076
1223
  }
1077
1224
 
1078
1225
  // src/commands/create-service-module/create-service-module.command.ts
1079
- import { join as join20, resolve as resolve2 } from "path";
1080
- import { access as access3 } from "fs/promises";
1226
+ import { join as join21, resolve as resolve3 } from "path";
1227
+ import { access as access4 } from "fs/promises";
1081
1228
 
1082
1229
  // src/commands/create-service-module/scaffold-service-module.ts
1083
1230
  import { fileURLToPath as fileURLToPath9 } from "url";
1084
- import { join as join19, dirname as dirname11 } from "path";
1085
- var nestjsServiceModuleTemplateDir2 = join19(
1231
+ import { join as join20, dirname as dirname11 } from "path";
1232
+ var nestjsServiceModuleTemplateDir2 = join20(
1086
1233
  dirname11(fileURLToPath9(import.meta.url)),
1087
1234
  "..",
1088
1235
  "templates",
@@ -1117,7 +1264,7 @@ function buildCustomServiceModuleEntry(organizationName, serviceName, serviceDis
1117
1264
  }
1118
1265
 
1119
1266
  // src/commands/create-service-module/append-docker-compose.ts
1120
- import { readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
1267
+ import { readFile as readFile9, writeFile as writeFile8 } from "fs/promises";
1121
1268
  async function appendServiceToDockerCompose(dockerComposePath, serviceName, platformName, applicationName, port, logger) {
1122
1269
  const block = `
1123
1270
  ${serviceName}:
@@ -1132,8 +1279,8 @@ async function appendServiceToDockerCompose(dockerComposePath, serviceName, plat
1132
1279
  - \${PWD}/:/app/out
1133
1280
  `;
1134
1281
  try {
1135
- const existing = await readFile8(dockerComposePath, "utf-8");
1136
- await writeFile7(dockerComposePath, existing + block, "utf-8");
1282
+ const existing = await readFile9(dockerComposePath, "utf-8");
1283
+ await writeFile8(dockerComposePath, existing + block, "utf-8");
1137
1284
  logger.log(`Updated docker-compose: ${dockerComposePath}`);
1138
1285
  } catch (err) {
1139
1286
  const message = err instanceof Error ? err.message : String(err);
@@ -1169,18 +1316,18 @@ async function createServiceModule(params, logger) {
1169
1316
  logger.log(`Error: The specified application "${applicationName}" is not registered in the product manifest.`);
1170
1317
  return;
1171
1318
  }
1172
- const applicationDir = resolve2(join20(rootDir, coreDirName), appEntry.localPath);
1319
+ const applicationDir = resolve3(join21(rootDir, coreDirName), appEntry.localPath);
1173
1320
  try {
1174
- await access3(applicationDir);
1321
+ await access4(applicationDir);
1175
1322
  } catch {
1176
1323
  logger.log(`Error: The specified application "${applicationName}" does not exist in the platform.`);
1177
1324
  return;
1178
1325
  }
1179
1326
  const suffix = serviceNameSuffix === void 0 ? "service" : serviceNameSuffix;
1180
1327
  const fullServiceName = suffix ? `${platformName}-${serviceName}-${suffix}` : `${platformName}-${serviceName}`;
1181
- const serviceDir = join20(applicationDir, "services", fullServiceName);
1328
+ const serviceDir = join21(applicationDir, "services", fullServiceName);
1182
1329
  try {
1183
- await access3(serviceDir);
1330
+ await access4(serviceDir);
1184
1331
  logger.log(`Error: A service named "${fullServiceName}" already exists in application "${applicationName}".`);
1185
1332
  return;
1186
1333
  } catch {
@@ -1199,21 +1346,21 @@ async function createServiceModule(params, logger) {
1199
1346
  logger.log(`Error: Could not scaffold NestJS service \u2014 ${formatError(err)}`);
1200
1347
  return;
1201
1348
  }
1202
- const bootstrapServiceDir = join20(applicationDir, "services", `${platformName}-${applicationName}-bootstrap-service`);
1349
+ const bootstrapServiceDir = join21(applicationDir, "services", `${platformName}-${applicationName}-bootstrap-service`);
1203
1350
  const moduleEntry = buildCustomServiceModuleEntry(organizationName, fullServiceName, serviceDisplayName);
1204
1351
  await addModuleEntry(bootstrapServiceDir, applicationName, moduleEntry, logger);
1205
- const dockerComposePath = join20(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
1352
+ const dockerComposePath = join21(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
1206
1353
  const port = await getNextAvailablePort(localDir);
1207
1354
  await appendServiceToDockerCompose(dockerComposePath, fullServiceName, platformName, applicationName, port, logger);
1208
1355
  logger.log(`Done! Service module "${fullServiceName}" added to application "${applicationName}".`);
1209
1356
  }
1210
1357
 
1211
1358
  // src/commands/create-ui-module/create-ui-module.command.ts
1212
- import { join as join21, resolve as resolve3 } from "path";
1213
- import { access as access4, readdir as readdir4 } from "fs/promises";
1359
+ import { join as join22, resolve as resolve4 } from "path";
1360
+ import { access as access5, readdir as readdir4 } from "fs/promises";
1214
1361
 
1215
1362
  // src/commands/create-ui-module/append-ui-docker-compose.ts
1216
- import { readFile as readFile9, writeFile as writeFile8 } from "fs/promises";
1363
+ import { readFile as readFile10, writeFile as writeFile9 } from "fs/promises";
1217
1364
  async function appendUiToDockerCompose(dockerComposePath, platformName, applicationName, port, logger, uiNameOverride) {
1218
1365
  const uiName = uiNameOverride ?? `${platformName}-${applicationName}-ui`;
1219
1366
  const block = `
@@ -1227,8 +1374,8 @@ async function appendUiToDockerCompose(dockerComposePath, platformName, applicat
1227
1374
  - \${PWD}/:/app/out
1228
1375
  `;
1229
1376
  try {
1230
- const existing = await readFile9(dockerComposePath, "utf-8");
1231
- await writeFile8(dockerComposePath, existing + block, "utf-8");
1377
+ const existing = await readFile10(dockerComposePath, "utf-8");
1378
+ await writeFile9(dockerComposePath, existing + block, "utf-8");
1232
1379
  logger.log(`Updated docker-compose: ${dockerComposePath}`);
1233
1380
  } catch (err) {
1234
1381
  const message = err instanceof Error ? err.message : String(err);
@@ -1264,14 +1411,14 @@ async function createUiModule(params, logger) {
1264
1411
  logger.log(`Error: The specified application "${applicationName}" is not registered in the product manifest.`);
1265
1412
  return;
1266
1413
  }
1267
- const applicationDir = resolve3(join21(rootDir, coreDirName), appEntry.localPath);
1414
+ const applicationDir = resolve4(join22(rootDir, coreDirName), appEntry.localPath);
1268
1415
  try {
1269
- await access4(applicationDir);
1416
+ await access5(applicationDir);
1270
1417
  } catch {
1271
1418
  logger.log(`Error: The specified application "${applicationName}" does not exist in the platform.`);
1272
1419
  return;
1273
1420
  }
1274
- const uiDir = join21(applicationDir, "ui");
1421
+ const uiDir = join22(applicationDir, "ui");
1275
1422
  try {
1276
1423
  const uiEntries = await readdir4(uiDir);
1277
1424
  const existingUiModules = uiEntries.filter((e) => e !== ".gitkeep");
@@ -1283,7 +1430,7 @@ async function createUiModule(params, logger) {
1283
1430
  }
1284
1431
  const uiSuffix = uiModuleSuffix === void 0 ? "ui" : uiModuleSuffix;
1285
1432
  const uiName = uiSuffix ? `${platformName}-${applicationName}-${uiSuffix}` : `${platformName}-${applicationName}`;
1286
- const uiOutputDir = join21(uiDir, uiName);
1433
+ const uiOutputDir = join22(uiDir, uiName);
1287
1434
  const uiBaseDir = `${platformName}-${applicationName}/ui`;
1288
1435
  try {
1289
1436
  await scaffoldUiModule(uiOutputDir, organizationName, platformName, applicationName, applicationDisplayName, uiBaseDir, logger);
@@ -1291,10 +1438,10 @@ async function createUiModule(params, logger) {
1291
1438
  logger.log(`Error: Could not scaffold UI module \u2014 ${formatError(err)}`);
1292
1439
  return;
1293
1440
  }
1294
- const bootstrapServiceDir = join21(applicationDir, "services", `${platformName}-${applicationName}-bootstrap-service`);
1441
+ const bootstrapServiceDir = join22(applicationDir, "services", `${platformName}-${applicationName}-bootstrap-service`);
1295
1442
  const moduleEntry = buildUiModuleEntry(organizationName, platformName, applicationName, applicationDisplayName, uiName);
1296
1443
  await addModuleEntry(bootstrapServiceDir, applicationName, moduleEntry, logger);
1297
- const dockerComposePath = join21(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
1444
+ const dockerComposePath = join22(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
1298
1445
  const port = await getNextAvailablePort(localDir);
1299
1446
  await appendUiToDockerCompose(dockerComposePath, platformName, applicationName, port, logger, uiName);
1300
1447
  logger.log(`Done! UI module "${uiName}" added to application "${applicationName}".`);
@@ -1302,14 +1449,14 @@ async function createUiModule(params, logger) {
1302
1449
 
1303
1450
  // src/commands/status/status-checks.ts
1304
1451
  import { spawn as spawn3 } from "child_process";
1305
- import { access as access7 } from "fs/promises";
1306
- import { join as join24, resolve as resolve5 } from "path";
1452
+ import { access as access8 } from "fs/promises";
1453
+ import { join as join25, resolve as resolve7 } from "path";
1307
1454
  import { fetch as undiciFetch2, Agent as Agent2 } from "undici";
1308
1455
 
1309
1456
  // src/commands/local-scripts/docker-compose-orchestrator.ts
1310
1457
  import { spawn } from "child_process";
1311
- import { access as access5 } from "fs/promises";
1312
- import { join as join22 } from "path";
1458
+ import { access as access6 } from "fs/promises";
1459
+ import { join as join23, resolve as resolve5 } from "path";
1313
1460
  function runDockerCompose(args2, logger, rootDir, signal) {
1314
1461
  return new Promise((resolvePromise) => {
1315
1462
  const child = spawn("docker", ["compose", ...args2], {
@@ -1372,20 +1519,29 @@ function captureDockerCompose(args2, rootDir) {
1372
1519
  child.on("error", (err) => reject(err));
1373
1520
  });
1374
1521
  }
1375
- async function getAppComposePaths(localDir, platformName, manifest) {
1522
+ async function getAppComposePaths(localDir, coreDir, platformName, manifest, logger) {
1376
1523
  const results = [];
1377
1524
  for (const app of manifest.applications) {
1378
- const prefixedPath = join22(localDir, `${platformName}-${app.name}-docker-compose.yml`);
1379
- const unprefixedPath = join22(localDir, `${app.name}-docker-compose.yml`);
1525
+ const appDir = resolve5(join23(coreDir), app.localPath);
1526
+ const newPath = join23(appDir, "docker-compose.yml");
1527
+ const legacyPrefixedPath = join23(localDir, `${platformName}-${app.name}-docker-compose.yml`);
1528
+ const legacyUnprefixedPath = join23(localDir, `${app.name}-docker-compose.yml`);
1380
1529
  let resolved = null;
1381
1530
  try {
1382
- await access5(prefixedPath);
1383
- resolved = prefixedPath;
1531
+ await access6(newPath);
1532
+ resolved = newPath;
1384
1533
  } catch {
1385
1534
  try {
1386
- await access5(unprefixedPath);
1387
- resolved = unprefixedPath;
1535
+ await access6(legacyPrefixedPath);
1536
+ resolved = legacyPrefixedPath;
1537
+ logger?.log(`Warning: docker-compose for "${app.name}" found in legacy location (core/local/). Consider moving it to the app monorepo root.`);
1388
1538
  } catch {
1539
+ try {
1540
+ await access6(legacyUnprefixedPath);
1541
+ resolved = legacyUnprefixedPath;
1542
+ logger?.log(`Warning: docker-compose for "${app.name}" found in legacy location (core/local/). Consider moving it to the app monorepo root.`);
1543
+ } catch {
1544
+ }
1389
1545
  }
1390
1546
  }
1391
1547
  if (resolved) {
@@ -1395,51 +1551,51 @@ async function getAppComposePaths(localDir, platformName, manifest) {
1395
1551
  return results;
1396
1552
  }
1397
1553
  async function buildFullComposeArgs(layout, manifest, logger) {
1398
- const { coreDirName, localDir } = layout;
1554
+ const { coreDirName, localDir, coreDir } = layout;
1399
1555
  const platformName = manifest.product.name;
1400
- const prefixedCoreCompose = join22(localDir, `${coreDirName}-docker-compose.yml`);
1401
- const unprefixedCoreCompose = join22(localDir, "core-docker-compose.yml");
1556
+ const prefixedCoreCompose = join23(localDir, `${coreDirName}-docker-compose.yml`);
1557
+ const unprefixedCoreCompose = join23(localDir, "core-docker-compose.yml");
1402
1558
  let coreComposePath;
1403
1559
  try {
1404
- await access5(prefixedCoreCompose);
1560
+ await access6(prefixedCoreCompose);
1405
1561
  coreComposePath = prefixedCoreCompose;
1406
1562
  } catch {
1407
1563
  coreComposePath = unprefixedCoreCompose;
1408
1564
  }
1409
1565
  const fileArgs = [
1410
1566
  "-f",
1411
- join22(localDir, "platform-docker-compose.yml"),
1567
+ join23(localDir, "platform-docker-compose.yml"),
1412
1568
  "-f",
1413
1569
  coreComposePath
1414
1570
  ];
1415
- const appEntries = await getAppComposePaths(localDir, platformName, manifest);
1571
+ const appEntries = await getAppComposePaths(localDir, coreDir, platformName, manifest, logger);
1416
1572
  for (const { composePath } of appEntries) {
1417
1573
  fileArgs.push("-f", composePath);
1418
1574
  }
1419
1575
  for (const app of manifest.applications) {
1420
1576
  if (!appEntries.find((e) => e.appName === app.name)) {
1421
- logger.log(`Warning: No docker-compose found for application "${app.name}" in ${coreDirName}/local/ \u2014 skipping.`);
1577
+ logger.log(`Warning: No docker-compose found for application "${app.name}" \u2014 skipping.`);
1422
1578
  }
1423
1579
  }
1424
1580
  return fileArgs;
1425
1581
  }
1426
1582
  async function buildSelectedComposeFiles(layout, selectedManifest, includeCore) {
1427
- const { coreDirName, localDir } = layout;
1583
+ const { coreDirName, localDir, coreDir } = layout;
1428
1584
  const platformName = selectedManifest.product.name;
1429
1585
  const files = [];
1430
1586
  if (includeCore) {
1431
- const prefixedCoreCompose = join22(localDir, `${coreDirName}-docker-compose.yml`);
1432
- const unprefixedCoreCompose = join22(localDir, "core-docker-compose.yml");
1587
+ const prefixedCoreCompose = join23(localDir, `${coreDirName}-docker-compose.yml`);
1588
+ const unprefixedCoreCompose = join23(localDir, "core-docker-compose.yml");
1433
1589
  let coreComposePath;
1434
1590
  try {
1435
- await access5(prefixedCoreCompose);
1591
+ await access6(prefixedCoreCompose);
1436
1592
  coreComposePath = prefixedCoreCompose;
1437
1593
  } catch {
1438
1594
  coreComposePath = unprefixedCoreCompose;
1439
1595
  }
1440
- files.push(join22(localDir, "platform-docker-compose.yml"), coreComposePath);
1596
+ files.push(join23(localDir, "platform-docker-compose.yml"), coreComposePath);
1441
1597
  }
1442
- const appEntries = await getAppComposePaths(localDir, platformName, selectedManifest);
1598
+ const appEntries = await getAppComposePaths(localDir, coreDir, platformName, selectedManifest);
1443
1599
  for (const { composePath } of appEntries) {
1444
1600
  files.push(composePath);
1445
1601
  }
@@ -1481,7 +1637,7 @@ async function getServicesFromComposeFiles(selectedFiles, allFiles, rootDir) {
1481
1637
  async function startEnvironment(layout, manifest, logger, signal, includeCore = true, fullManifest) {
1482
1638
  const { rootDir, localDir } = layout;
1483
1639
  const platformName = manifest.product.name;
1484
- const envFile = join22(localDir, ".env");
1640
+ const envFile = join23(localDir, ".env");
1485
1641
  const isSelective = fullManifest !== void 0;
1486
1642
  const fileArgs = await buildFullComposeArgs(layout, isSelective ? fullManifest : manifest, logger);
1487
1643
  const projectArgs = ["-p", `${platformName}-platform`, "--project-directory", localDir, "--env-file", envFile];
@@ -1503,7 +1659,7 @@ async function startEnvironment(layout, manifest, logger, signal, includeCore =
1503
1659
  async function stopEnvironment(layout, manifest, logger, signal, includeCore = true, fullManifest) {
1504
1660
  const { rootDir, localDir } = layout;
1505
1661
  const platformName = manifest.product.name;
1506
- const envFile = join22(localDir, ".env");
1662
+ const envFile = join23(localDir, ".env");
1507
1663
  const isSelective = fullManifest !== void 0;
1508
1664
  const fileArgs = await buildFullComposeArgs(layout, isSelective ? fullManifest : manifest, logger);
1509
1665
  const projectArgs = ["-p", `${platformName}-platform`, "--project-directory", localDir, "--env-file", envFile];
@@ -1522,10 +1678,18 @@ async function stopEnvironment(layout, manifest, logger, signal, includeCore = t
1522
1678
  await runDockerCompose([...projectArgs, ...fileArgs, "down"], logger, rootDir, signal);
1523
1679
  }
1524
1680
  }
1525
- async function destroyEnvironment(layout, manifest, logger, signal, includeCore = true, fullManifest) {
1681
+ async function resetEnvironment(layout, manifest, logger, signal) {
1526
1682
  const { rootDir, localDir } = layout;
1527
1683
  const platformName = manifest.product.name;
1528
- const envFile = join22(localDir, ".env");
1684
+ const envFile = join23(localDir, ".env");
1685
+ const fileArgs = await buildFullComposeArgs(layout, manifest, logger);
1686
+ const projectArgs = ["-p", `${platformName}-platform`, "--project-directory", localDir, "--env-file", envFile];
1687
+ await runDockerCompose([...projectArgs, ...fileArgs, "down", "-v"], logger, rootDir, signal);
1688
+ }
1689
+ async function destroyEnvironment(layout, manifest, logger, signal, includeCore = true, fullManifest, removeImages = true) {
1690
+ const { rootDir, localDir } = layout;
1691
+ const platformName = manifest.product.name;
1692
+ const envFile = join23(localDir, ".env");
1529
1693
  const isSelective = fullManifest !== void 0;
1530
1694
  const fileArgs = await buildFullComposeArgs(layout, isSelective ? fullManifest : manifest, logger);
1531
1695
  const projectArgs = ["-p", `${platformName}-platform`, "--project-directory", localDir, "--env-file", envFile];
@@ -1544,14 +1708,15 @@ async function destroyEnvironment(layout, manifest, logger, signal, includeCore
1544
1708
  }
1545
1709
  } else {
1546
1710
  logger.log("Destroying environment...");
1547
- await runDockerCompose([...projectArgs, ...fileArgs, "down", "-v", "--rmi", "all"], logger, rootDir, signal);
1711
+ const downArgs = removeImages ? ["down", "-v", "--rmi", "local"] : ["down", "-v"];
1712
+ await runDockerCompose([...projectArgs, ...fileArgs, ...downArgs], logger, rootDir, signal);
1548
1713
  }
1549
1714
  }
1550
1715
 
1551
1716
  // src/commands/local-scripts/npm-orchestrator.ts
1552
1717
  import { spawn as spawn2 } from "child_process";
1553
- import { access as access6 } from "fs/promises";
1554
- import { resolve as resolve4, join as join23 } from "path";
1718
+ import { access as access7 } from "fs/promises";
1719
+ import { resolve as resolve6, join as join24 } from "path";
1555
1720
  function runCommand(command, args2, workDir, logger, signal) {
1556
1721
  return new Promise((resolvePromise) => {
1557
1722
  const child = spawn2(command, args2, {
@@ -1598,7 +1763,7 @@ function runCommand(command, args2, workDir, logger, signal) {
1598
1763
  }
1599
1764
  async function dirExists(dirPath) {
1600
1765
  try {
1601
- await access6(dirPath);
1766
+ await access7(dirPath);
1602
1767
  return true;
1603
1768
  } catch {
1604
1769
  return false;
@@ -1608,7 +1773,7 @@ async function installDependencies(layout, manifest, logger, signal, includeCore
1608
1773
  const { coreDir, coreDirName } = layout;
1609
1774
  const appDirs = [];
1610
1775
  for (const app of manifest.applications) {
1611
- const appDir = resolve4(join23(coreDir), app.localPath);
1776
+ const appDir = resolve6(join24(coreDir), app.localPath);
1612
1777
  if (!await dirExists(appDir)) {
1613
1778
  logger.log(`Warning: Application directory "${app.name}" not found at ${appDir} \u2014 skipping install.`);
1614
1779
  continue;
@@ -1634,7 +1799,7 @@ async function buildAll(layout, manifest, logger, signal, includeCore = true) {
1634
1799
  }
1635
1800
  const appDirs = [];
1636
1801
  for (const app of manifest.applications) {
1637
- const appDir = resolve4(join23(coreDir), app.localPath);
1802
+ const appDir = resolve6(join24(coreDir), app.localPath);
1638
1803
  if (!await dirExists(appDir)) {
1639
1804
  logger.log(`Warning: Application directory "${app.name}" not found at ${appDir} \u2014 skipping build.`);
1640
1805
  continue;
@@ -1746,10 +1911,10 @@ async function runLocalScript(scriptName, logger, signal, appNames) {
1746
1911
  }
1747
1912
 
1748
1913
  // src/commands/status/status-checks.ts
1749
- function spawnCapture(cmd, args2, cwd5) {
1914
+ function spawnCapture(cmd, args2, cwd11) {
1750
1915
  return new Promise((resolvePromise) => {
1751
1916
  const child = spawn3(cmd, args2, {
1752
- cwd: cwd5,
1917
+ cwd: cwd11,
1753
1918
  shell: false,
1754
1919
  stdio: ["ignore", "pipe", "pipe"]
1755
1920
  });
@@ -1771,7 +1936,7 @@ function spawnCapture(cmd, args2, cwd5) {
1771
1936
  }
1772
1937
  async function pathExists(p) {
1773
1938
  try {
1774
- await access7(p);
1939
+ await access8(p);
1775
1940
  return true;
1776
1941
  } catch {
1777
1942
  return false;
@@ -1815,13 +1980,13 @@ async function checkInstalled(layout, manifest) {
1815
1980
  const results = [];
1816
1981
  const coreCheck = {
1817
1982
  name: layout.coreDirName,
1818
- path: join24(layout.coreDir, "node_modules"),
1819
- exists: await pathExists(join24(layout.coreDir, "node_modules"))
1983
+ path: join25(layout.coreDir, "node_modules"),
1984
+ exists: await pathExists(join25(layout.coreDir, "node_modules"))
1820
1985
  };
1821
1986
  results.push(coreCheck);
1822
1987
  for (const app of manifest.applications) {
1823
- const appDir = resolve5(layout.coreDir, app.localPath);
1824
- const nodeModulesPath = join24(appDir, "node_modules");
1988
+ const appDir = resolve7(layout.coreDir, app.localPath);
1989
+ const nodeModulesPath = join25(appDir, "node_modules");
1825
1990
  results.push({
1826
1991
  name: app.name,
1827
1992
  path: nodeModulesPath,
@@ -1832,15 +1997,15 @@ async function checkInstalled(layout, manifest) {
1832
1997
  }
1833
1998
  async function checkBuilt(layout, manifest) {
1834
1999
  const results = [];
1835
- const coreTurboPath = join24(layout.coreDir, ".turbo");
2000
+ const coreTurboPath = join25(layout.coreDir, ".turbo");
1836
2001
  results.push({
1837
2002
  name: layout.coreDirName,
1838
2003
  path: coreTurboPath,
1839
2004
  exists: await pathExists(coreTurboPath)
1840
2005
  });
1841
2006
  for (const app of manifest.applications) {
1842
- const appDir = resolve5(layout.coreDir, app.localPath);
1843
- const appTurboPath = join24(appDir, ".turbo");
2007
+ const appDir = resolve7(layout.coreDir, app.localPath);
2008
+ const appTurboPath = join25(appDir, ".turbo");
1844
2009
  results.push({
1845
2010
  name: app.name,
1846
2011
  path: appTurboPath,
@@ -1852,13 +2017,13 @@ async function checkBuilt(layout, manifest) {
1852
2017
  async function resolveComposeFiles(layout, manifest) {
1853
2018
  const { localDir, coreDirName } = layout;
1854
2019
  const platformName = manifest.product.name;
1855
- const files = [join24(localDir, "platform-docker-compose.yml")];
1856
- const prefixedCore = join24(localDir, `${coreDirName}-docker-compose.yml`);
1857
- const unprefixedCore = join24(localDir, "core-docker-compose.yml");
2020
+ const files = [join25(localDir, "platform-docker-compose.yml")];
2021
+ const prefixedCore = join25(localDir, `${coreDirName}-docker-compose.yml`);
2022
+ const unprefixedCore = join25(localDir, "core-docker-compose.yml");
1858
2023
  files.push(await pathExists(prefixedCore) ? prefixedCore : unprefixedCore);
1859
2024
  for (const app of manifest.applications) {
1860
- const prefixed = join24(localDir, `${platformName}-${app.name}-docker-compose.yml`);
1861
- const unprefixed = join24(localDir, `${app.name}-docker-compose.yml`);
2025
+ const prefixed = join25(localDir, `${platformName}-${app.name}-docker-compose.yml`);
2026
+ const unprefixed = join25(localDir, `${app.name}-docker-compose.yml`);
1862
2027
  if (await pathExists(prefixed)) files.push(prefixed);
1863
2028
  else if (await pathExists(unprefixed)) files.push(unprefixed);
1864
2029
  }
@@ -1964,15 +2129,15 @@ async function getServicesForComposeFiles(selectedFiles, allFiles, rootDir) {
1964
2129
  async function checkContainersPerApp(layout, manifest, allContainers) {
1965
2130
  const { localDir, coreDirName, rootDir } = layout;
1966
2131
  const platformName = manifest.product.name;
1967
- const prefixedCore = join24(localDir, `${coreDirName}-docker-compose.yml`);
1968
- const unprefixedCore = join24(localDir, "core-docker-compose.yml");
2132
+ const prefixedCore = join25(localDir, `${coreDirName}-docker-compose.yml`);
2133
+ const unprefixedCore = join25(localDir, "core-docker-compose.yml");
1969
2134
  const coreComposePath = await pathExists(prefixedCore) ? prefixedCore : unprefixedCore;
1970
- const platformComposePath = join24(localDir, "platform-docker-compose.yml");
2135
+ const platformComposePath = join25(localDir, "platform-docker-compose.yml");
1971
2136
  const allFiles = [platformComposePath, coreComposePath];
1972
2137
  const appComposeMap = /* @__PURE__ */ new Map();
1973
2138
  for (const app of manifest.applications) {
1974
- const prefixed = join24(localDir, `${platformName}-${app.name}-docker-compose.yml`);
1975
- const unprefixed = join24(localDir, `${app.name}-docker-compose.yml`);
2139
+ const prefixed = join25(localDir, `${platformName}-${app.name}-docker-compose.yml`);
2140
+ const unprefixed = join25(localDir, `${app.name}-docker-compose.yml`);
1976
2141
  if (await pathExists(prefixed)) {
1977
2142
  appComposeMap.set(app.name, prefixed);
1978
2143
  allFiles.push(prefixed);
@@ -2019,7 +2184,7 @@ async function checkContainersPerApp(layout, manifest, allContainers) {
2019
2184
  return groups;
2020
2185
  }
2021
2186
  async function checkIdpProviders(layout) {
2022
- const envPath = join24(layout.localDir, ".env");
2187
+ const envPath = join25(layout.localDir, ".env");
2023
2188
  let env;
2024
2189
  try {
2025
2190
  env = await readEnvFile(envPath);
@@ -2138,7 +2303,7 @@ var statusCommand = {
2138
2303
  };
2139
2304
 
2140
2305
  // src/commands/manage-platform-admins/manage-platform-admins.command.ts
2141
- import { join as join25 } from "path";
2306
+ import { join as join26 } from "path";
2142
2307
  import { fetch as undiciFetch3, Agent as Agent3 } from "undici";
2143
2308
  var MANAGE_PLATFORM_ADMINS_COMMAND_NAME = "manage-platform-admins";
2144
2309
  var managePlatformAdminsCommand = {
@@ -2152,7 +2317,7 @@ async function getGatewayConfig(logger) {
2152
2317
  logger.log("Error: Cannot manage platform admins \u2014 no platform initialized in this directory.");
2153
2318
  return null;
2154
2319
  }
2155
- const envPath = join25(layout.localDir, ".env");
2320
+ const envPath = join26(layout.localDir, ".env");
2156
2321
  let env;
2157
2322
  try {
2158
2323
  env = await readEnvFile(envPath);
@@ -2355,6 +2520,46 @@ async function installAiPlugin(params, context) {
2355
2520
  }
2356
2521
  }
2357
2522
 
2523
+ // src/commands/standalone/standalone.command.ts
2524
+ var STANDALONE_COMMAND_NAME = "standalone";
2525
+ var STANDALONE_STOP_COMMAND_NAME = "standalone-stop";
2526
+ var standaloneCommand = {
2527
+ name: STANDALONE_COMMAND_NAME,
2528
+ description: "Start platform in standalone mode for this application",
2529
+ hidden: (ctx) => !ctx.inAppMonorepo
2530
+ };
2531
+ var standaloneStopCommand = {
2532
+ name: STANDALONE_STOP_COMMAND_NAME,
2533
+ description: "Stop standalone platform",
2534
+ hidden: (ctx) => !ctx.inAppMonorepo
2535
+ };
2536
+
2537
+ // src/commands/standalone-config/standalone-config.command.ts
2538
+ var STANDALONE_CONFIG_SHOW_COMMAND_NAME = "standalone-config-show";
2539
+ var STANDALONE_CONFIG_SET_COMMAND_NAME = "standalone-config-set";
2540
+ var STANDALONE_CONFIG_DELETE_COMMAND_NAME = "standalone-config-delete";
2541
+ var STANDALONE_CONFIG_LIST_COMMAND_NAME = "standalone-config-list";
2542
+ var standaloneConfigShowCommand = {
2543
+ name: STANDALONE_CONFIG_SHOW_COMMAND_NAME,
2544
+ description: "Show resolved standalone config for current app",
2545
+ hidden: () => false
2546
+ };
2547
+ var standaloneConfigSetCommand = {
2548
+ name: STANDALONE_CONFIG_SET_COMMAND_NAME,
2549
+ description: "Edit standalone config defaults or app overrides",
2550
+ hidden: () => false
2551
+ };
2552
+ var standaloneConfigDeleteCommand = {
2553
+ name: STANDALONE_CONFIG_DELETE_COMMAND_NAME,
2554
+ description: "Delete a saved standalone app config entry",
2555
+ hidden: () => false
2556
+ };
2557
+ var standaloneConfigListCommand = {
2558
+ name: STANDALONE_CONFIG_LIST_COMMAND_NAME,
2559
+ description: "List all apps with standalone config",
2560
+ hidden: () => false
2561
+ };
2562
+
2358
2563
  // src/commands/registry.ts
2359
2564
  var CommandRegistry = class {
2360
2565
  commands = /* @__PURE__ */ new Map();
@@ -2396,6 +2601,12 @@ for (const cmd of localScriptCommands) {
2396
2601
  registry.register(statusCommand);
2397
2602
  registry.register(managePlatformAdminsCommand);
2398
2603
  registry.register(installAiPluginCommand);
2604
+ registry.register(standaloneCommand);
2605
+ registry.register(standaloneStopCommand);
2606
+ registry.register(standaloneConfigShowCommand);
2607
+ registry.register(standaloneConfigSetCommand);
2608
+ registry.register(standaloneConfigDeleteCommand);
2609
+ registry.register(standaloneConfigListCommand);
2399
2610
 
2400
2611
  // src/app-state.ts
2401
2612
  var APP_STATE = {
@@ -2415,9 +2626,20 @@ async function createApplicationService(params, logger) {
2415
2626
 
2416
2627
  // src/controllers/ui/create-application.ui-controller.ts
2417
2628
  async function createApplicationUiController(ctx) {
2418
- if (!await isPlatformInitialized()) {
2419
- ctx.log("Error: Cannot create an application \u2014 no platform initialized in this directory.");
2420
- return;
2629
+ const insidePlatform = await isPlatformInitialized();
2630
+ let platformName;
2631
+ let organizationName;
2632
+ if (!insidePlatform) {
2633
+ platformName = await ctx.prompt("Platform name (e.g. myplatform):");
2634
+ if (!/^[a-z0-9-]+$/.test(platformName)) {
2635
+ ctx.log("Error: Platform name must use only lowercase letters, numbers, and hyphens.");
2636
+ return;
2637
+ }
2638
+ organizationName = await ctx.prompt("Organization name (npm scope, e.g. myorg):");
2639
+ if (!/^[a-z0-9-]+$/.test(organizationName)) {
2640
+ ctx.log("Error: Organization name must use only lowercase letters, numbers, and hyphens.");
2641
+ return;
2642
+ }
2421
2643
  }
2422
2644
  const applicationName = await ctx.prompt("Application name:");
2423
2645
  if (!/^[a-z0-9-]+$/.test(applicationName)) {
@@ -2434,7 +2656,9 @@ async function createApplicationUiController(ctx) {
2434
2656
  applicationDisplayName,
2435
2657
  applicationDescription,
2436
2658
  hasUserInterface,
2437
- hasBackendService
2659
+ hasBackendService,
2660
+ platformName,
2661
+ organizationName
2438
2662
  },
2439
2663
  ctx
2440
2664
  );
@@ -2913,29 +3137,29 @@ async function installAiPluginService(params, context) {
2913
3137
  }
2914
3138
 
2915
3139
  // src/commands/install-ai-plugin/providers/claude.provider.ts
2916
- import { homedir as homedir2 } from "os";
2917
- import { join as join26 } from "path";
2918
- import { readFile as readFile10, writeFile as writeFile9, mkdir as mkdir3, rm } from "fs/promises";
3140
+ import { homedir as homedir3 } from "os";
3141
+ import { join as join27 } from "path";
3142
+ import { readFile as readFile11, writeFile as writeFile10, mkdir as mkdir4, rm } from "fs/promises";
2919
3143
  var ClaudeProvider = class {
2920
3144
  name = "claude";
2921
- baseDir = join26(homedir2(), ".claude");
3145
+ baseDir = join27(homedir3(), ".claude");
2922
3146
  getInstallPath(skillName) {
2923
- return join26(this.baseDir, "skills", skillName, "SKILL.md");
3147
+ return join27(this.baseDir, "skills", skillName, "SKILL.md");
2924
3148
  }
2925
3149
  manifestPath(pluginName) {
2926
- return join26(this.baseDir, `${pluginName}.manifest.json`);
3150
+ return join27(this.baseDir, `${pluginName}.manifest.json`);
2927
3151
  }
2928
3152
  async readManifest(pluginName) {
2929
3153
  try {
2930
- const content = await readFile10(this.manifestPath(pluginName), "utf-8");
3154
+ const content = await readFile11(this.manifestPath(pluginName), "utf-8");
2931
3155
  return JSON.parse(content);
2932
3156
  } catch {
2933
3157
  return null;
2934
3158
  }
2935
3159
  }
2936
3160
  async writeManifest(manifest) {
2937
- await mkdir3(this.baseDir, { recursive: true });
2938
- await writeFile9(
3161
+ await mkdir4(this.baseDir, { recursive: true });
3162
+ await writeFile10(
2939
3163
  this.manifestPath(manifest.plugin),
2940
3164
  JSON.stringify(manifest, null, 2),
2941
3165
  "utf-8"
@@ -2955,16 +3179,16 @@ var ClaudeProvider = class {
2955
3179
  }
2956
3180
  async installSkill(skill, docsSource, logger) {
2957
3181
  const sourcePath = docsSource.resolve(skill.sourceFile);
2958
- let content = await readFile10(sourcePath, "utf-8");
3182
+ let content = await readFile11(sourcePath, "utf-8");
2959
3183
  const docsPath = docsSource.resolve("docs");
2960
3184
  content = content.replaceAll("{{docsPath}}", docsPath);
2961
3185
  const installPath = this.getInstallPath(skill.name);
2962
- await mkdir3(join26(installPath, ".."), { recursive: true });
2963
- await writeFile9(installPath, content, "utf-8");
3186
+ await mkdir4(join27(installPath, ".."), { recursive: true });
3187
+ await writeFile10(installPath, content, "utf-8");
2964
3188
  logger.log(` Installed skill: ${installPath}`);
2965
3189
  }
2966
3190
  async removeSkill(skillName, logger) {
2967
- const skillDir = join26(this.baseDir, "skills", skillName);
3191
+ const skillDir = join27(this.baseDir, "skills", skillName);
2968
3192
  try {
2969
3193
  await rm(skillDir, { recursive: true, force: true });
2970
3194
  logger.log(` Removed skill: ${skillDir}`);
@@ -2972,11 +3196,11 @@ var ClaudeProvider = class {
2972
3196
  }
2973
3197
  }
2974
3198
  async updatePermissions(docsSource, logger) {
2975
- const settingsPath = join26(this.baseDir, "settings.json");
3199
+ const settingsPath = join27(this.baseDir, "settings.json");
2976
3200
  const docsPath = docsSource.resolve("docs");
2977
3201
  let settings = {};
2978
3202
  try {
2979
- const content = await readFile10(settingsPath, "utf-8");
3203
+ const content = await readFile11(settingsPath, "utf-8");
2980
3204
  settings = JSON.parse(content);
2981
3205
  } catch {
2982
3206
  }
@@ -2987,8 +3211,8 @@ var ClaudeProvider = class {
2987
3211
  }
2988
3212
  permissions.additionalDirectories = [...additionalDirectories, docsPath];
2989
3213
  settings.permissions = permissions;
2990
- await mkdir3(this.baseDir, { recursive: true });
2991
- await writeFile9(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
3214
+ await mkdir4(this.baseDir, { recursive: true });
3215
+ await writeFile10(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
2992
3216
  logger.log(` Granted read access to docs: ${docsPath}`);
2993
3217
  }
2994
3218
  };
@@ -3008,11 +3232,11 @@ function getProvider(name) {
3008
3232
 
3009
3233
  // src/commands/install-ai-plugin/docs-source/local.docs-source.ts
3010
3234
  import { fileURLToPath as fileURLToPath10 } from "url";
3011
- import { join as join27, dirname as dirname12 } from "path";
3012
- var packageRoot = join27(dirname12(fileURLToPath10(import.meta.url)), "..");
3235
+ import { join as join28, dirname as dirname12 } from "path";
3236
+ var packageRoot = join28(dirname12(fileURLToPath10(import.meta.url)), "..");
3013
3237
  var LocalDocsSource = class {
3014
3238
  resolve(relativePath) {
3015
- return join27(packageRoot, relativePath);
3239
+ return join28(packageRoot, relativePath);
3016
3240
  }
3017
3241
  };
3018
3242
 
@@ -3054,6 +3278,837 @@ async function installAiPluginUiController(ctx) {
3054
3278
  );
3055
3279
  }
3056
3280
 
3281
+ // src/controllers/ui/standalone.ui-controller.ts
3282
+ import { rm as rm3 } from "fs/promises";
3283
+ import { join as join32 } from "path";
3284
+ import { cwd as cwd7 } from "process";
3285
+ import { exec } from "child_process";
3286
+
3287
+ // src/commands/standalone/app-monorepo-check.ts
3288
+ import { access as access9, readdir as readdir5 } from "fs/promises";
3289
+ import { join as join29 } from "path";
3290
+ import { cwd as cwd6 } from "process";
3291
+ async function findAppMonorepoLayout(startDir = cwd6()) {
3292
+ const composePath = join29(startDir, "docker-compose.yml");
3293
+ const packageJsonPath = join29(startDir, "package.json");
3294
+ try {
3295
+ await access9(composePath);
3296
+ await access9(packageJsonPath);
3297
+ } catch {
3298
+ return null;
3299
+ }
3300
+ if (await hasCoreChildDir(startDir)) {
3301
+ return null;
3302
+ }
3303
+ return { appDir: startDir, composePath };
3304
+ }
3305
+ async function hasCoreChildDir(dir) {
3306
+ try {
3307
+ const entries = await readdir5(dir, { withFileTypes: true });
3308
+ for (const entry of entries) {
3309
+ if (entry.isDirectory() && (entry.name.endsWith("-core") || entry.name === "core")) {
3310
+ try {
3311
+ await access9(join29(dir, entry.name, "product.manifest.json"));
3312
+ return true;
3313
+ } catch {
3314
+ }
3315
+ }
3316
+ }
3317
+ } catch {
3318
+ }
3319
+ return false;
3320
+ }
3321
+ async function isInAppMonorepo() {
3322
+ return await findAppMonorepoLayout() !== null;
3323
+ }
3324
+
3325
+ // src/commands/standalone/standalone-auto-detect.ts
3326
+ import { readdir as readdir6, readFile as readFile12 } from "fs/promises";
3327
+ import { join as join30, basename as basename2 } from "path";
3328
+ async function autoDetectAppIdentity(appDir) {
3329
+ const fromBootstrap = await detectFromBootstrap(appDir);
3330
+ if (fromBootstrap) return fromBootstrap;
3331
+ return detectFromPackageJson(appDir);
3332
+ }
3333
+ async function detectFromBootstrap(appDir) {
3334
+ const servicesDir = join30(appDir, "services");
3335
+ let entries;
3336
+ try {
3337
+ entries = await readdir6(servicesDir);
3338
+ } catch {
3339
+ return null;
3340
+ }
3341
+ const bootstrapDirs = entries.filter((e) => e.endsWith("-bootstrap-service"));
3342
+ if (bootstrapDirs.length === 0) return null;
3343
+ for (const bootstrapDirName of bootstrapDirs) {
3344
+ const dataDir = join30(servicesDir, bootstrapDirName, "src", "data");
3345
+ let dataDirEntries;
3346
+ try {
3347
+ dataDirEntries = await readdir6(dataDir);
3348
+ } catch {
3349
+ continue;
3350
+ }
3351
+ for (const subDir of dataDirEntries) {
3352
+ if (subDir === "platform") continue;
3353
+ const appJsonPath = join30(dataDir, subDir, "application.json");
3354
+ try {
3355
+ const content = await readFile12(appJsonPath, "utf-8");
3356
+ const appData = JSON.parse(content);
3357
+ if (!appData.name) continue;
3358
+ const applicationName = appData.name;
3359
+ const applicationDisplayName = appData.displayName ?? applicationName;
3360
+ const suffix = `-${applicationName}-bootstrap-service`;
3361
+ const platformName = bootstrapDirName.endsWith(suffix) ? bootstrapDirName.slice(0, -suffix.length) : inferPlatformNameFromAppDir(appDir, applicationName);
3362
+ if (!platformName) continue;
3363
+ return { platformName, applicationName, applicationDisplayName };
3364
+ } catch {
3365
+ continue;
3366
+ }
3367
+ }
3368
+ }
3369
+ return null;
3370
+ }
3371
+ async function detectFromPackageJson(appDir) {
3372
+ try {
3373
+ const content = await readFile12(join30(appDir, "package.json"), "utf-8");
3374
+ const pkg = JSON.parse(content);
3375
+ const rawName = pkg.name ?? "";
3376
+ const applicationName = rawName.includes("/") ? rawName.split("/")[1] ?? rawName : rawName;
3377
+ if (!applicationName) return null;
3378
+ const applicationDisplayName = pkg.description ?? applicationName;
3379
+ const platformName = inferPlatformNameFromAppDir(appDir, applicationName);
3380
+ if (!platformName) return null;
3381
+ return { platformName, applicationName, applicationDisplayName };
3382
+ } catch {
3383
+ return null;
3384
+ }
3385
+ }
3386
+ function inferPlatformNameFromAppDir(appDir, applicationName) {
3387
+ const dirName = basename2(appDir);
3388
+ const suffix = `-${applicationName}`;
3389
+ if (dirName.endsWith(suffix) && dirName.length > suffix.length) {
3390
+ return dirName.slice(0, -suffix.length);
3391
+ }
3392
+ return null;
3393
+ }
3394
+
3395
+ // src/commands/standalone/standalone-orchestrator.ts
3396
+ import { join as join31, dirname as dirname13 } from "path";
3397
+ import { mkdir as mkdir5, rm as rm2, readdir as readdir7, access as access10, writeFile as writeFile11, readFile as readFile13 } from "fs/promises";
3398
+ import { fetch as undiciFetch4, Agent as Agent4 } from "undici";
3399
+ var TMP_BASE = "/tmp";
3400
+ var STANDALONE_PREFIX = "platform-standalone-";
3401
+ var DETACH_MARKER = ".standalone-running";
3402
+ function getTmpDir(platformName) {
3403
+ return join31(TMP_BASE, `${STANDALONE_PREFIX}${platformName}`);
3404
+ }
3405
+ async function findRunningSingleton() {
3406
+ try {
3407
+ const entries = await readdir7(TMP_BASE, { withFileTypes: true });
3408
+ for (const entry of entries) {
3409
+ if (entry.isDirectory() && entry.name.startsWith(STANDALONE_PREFIX)) {
3410
+ const candidateDir = join31(TMP_BASE, entry.name);
3411
+ try {
3412
+ await access10(join31(candidateDir, DETACH_MARKER));
3413
+ return candidateDir;
3414
+ } catch {
3415
+ }
3416
+ }
3417
+ }
3418
+ } catch {
3419
+ }
3420
+ return null;
3421
+ }
3422
+ async function cleanupTmpDir(tmpDir, logger) {
3423
+ logger.log(`Cleaning up temporary directory: ${tmpDir}`);
3424
+ try {
3425
+ await rm2(tmpDir, { recursive: true, force: true });
3426
+ logger.log("Temporary directory removed.");
3427
+ } catch (err) {
3428
+ logger.log(`Warning: Could not remove temporary directory \u2014 ${err.message}`);
3429
+ }
3430
+ }
3431
+ async function waitForGateway(gatewayUrl, logger, timeoutMs = 12e4) {
3432
+ const healthUrl = `${gatewayUrl}/health`;
3433
+ const agent = new Agent4({ connect: { rejectUnauthorized: false } });
3434
+ const interval = 3e3;
3435
+ const start = Date.now();
3436
+ logger.log("Waiting for gateway to be healthy...");
3437
+ while (Date.now() - start < timeoutMs) {
3438
+ try {
3439
+ const res = await undiciFetch4(healthUrl, { dispatcher: agent });
3440
+ if (res.ok) {
3441
+ logger.log("Gateway is healthy.");
3442
+ return true;
3443
+ }
3444
+ } catch {
3445
+ }
3446
+ await new Promise((r) => setTimeout(r, interval));
3447
+ }
3448
+ logger.log("Warning: Gateway did not become healthy within the timeout.");
3449
+ return false;
3450
+ }
3451
+ async function configureIdpFromConfig(config, localDir, logger) {
3452
+ if (!config.idp) {
3453
+ logger.log("No IDP configured \u2014 run 'platform standalone-config set' to add an IDP. Skipping IDP setup.");
3454
+ return;
3455
+ }
3456
+ const envPath = join31(localDir, ".env");
3457
+ let env;
3458
+ try {
3459
+ env = await readEnvFile(envPath);
3460
+ } catch (err) {
3461
+ logger.log(`Warning: Could not read .env for IDP setup \u2014 ${err.message}`);
3462
+ return;
3463
+ }
3464
+ const gatewayUrl = env.get("PAE_GATEWAY_HOST_URL");
3465
+ const accessSecret = env.get("PAE_GATEWAY_SERVICE_ACCESS_SECRET");
3466
+ if (!gatewayUrl || !accessSecret) {
3467
+ logger.log("Warning: Missing PAE_GATEWAY_HOST_URL or PAE_GATEWAY_SERVICE_ACCESS_SECRET \u2014 skipping IDP setup.");
3468
+ return;
3469
+ }
3470
+ const provider = idpProviderRegistry.get(config.idp.provider);
3471
+ if (!provider) {
3472
+ logger.log(`Warning: Unknown IDP provider "${config.idp.provider}" \u2014 skipping IDP setup.`);
3473
+ return;
3474
+ }
3475
+ const payload = provider.buildPayload({
3476
+ providerType: config.idp.provider,
3477
+ name: config.idp.name,
3478
+ issuer: config.idp.issuer,
3479
+ clientId: config.idp.clientId,
3480
+ clientSecret: config.idp.clientSecret,
3481
+ extras: config.idp.extras ?? {}
3482
+ });
3483
+ const agent = new Agent4({ connect: { rejectUnauthorized: false } });
3484
+ try {
3485
+ const response = await undiciFetch4(`${gatewayUrl}/_/providers`, {
3486
+ method: "POST",
3487
+ headers: {
3488
+ "Content-Type": "application/json",
3489
+ Accept: "application/json",
3490
+ Authorization: `Bearer ${accessSecret}`
3491
+ },
3492
+ body: JSON.stringify(payload),
3493
+ dispatcher: agent
3494
+ });
3495
+ if (!response.ok) {
3496
+ const body = await response.text().catch(() => "");
3497
+ logger.log(`Warning: IDP configuration failed \u2014 ${response.status}${body ? `: ${body}` : ""}`);
3498
+ } else {
3499
+ logger.log(`IDP provider "${config.idp.name}" configured successfully.`);
3500
+ }
3501
+ } catch (err) {
3502
+ const error = err;
3503
+ const cause = error.cause instanceof Error ? ` (cause: ${error.cause.message})` : "";
3504
+ logger.log(`Warning: Could not reach gateway for IDP setup \u2014 ${error.message}${cause}`);
3505
+ }
3506
+ }
3507
+ async function configureAdminUsersFromConfig(config, localDir, logger) {
3508
+ if (!config.adminUsers || config.adminUsers.length === 0) {
3509
+ return;
3510
+ }
3511
+ const envPath = join31(localDir, ".env");
3512
+ let env;
3513
+ try {
3514
+ env = await readEnvFile(envPath);
3515
+ } catch (err) {
3516
+ logger.log(`Warning: Could not read .env for admin setup \u2014 ${err.message}`);
3517
+ return;
3518
+ }
3519
+ const gatewayUrl = env.get("PAE_GATEWAY_HOST_URL");
3520
+ const accessSecret = env.get("PAE_GATEWAY_SERVICE_ACCESS_SECRET");
3521
+ if (!gatewayUrl || !accessSecret) {
3522
+ logger.log("Warning: Missing PAE_GATEWAY_HOST_URL or PAE_GATEWAY_SERVICE_ACCESS_SECRET \u2014 skipping admin setup.");
3523
+ return;
3524
+ }
3525
+ const agent = new Agent4({ connect: { rejectUnauthorized: false } });
3526
+ for (const username of config.adminUsers) {
3527
+ try {
3528
+ const response = await undiciFetch4(`${gatewayUrl}/applications/platform/rules`, {
3529
+ method: "POST",
3530
+ headers: {
3531
+ "Content-Type": "application/json",
3532
+ Accept: "application/json",
3533
+ Authorization: `Bearer ${accessSecret}`
3534
+ },
3535
+ body: JSON.stringify({ subject: username, subjectType: "user", resourceType: "role", resource: "admin" }),
3536
+ dispatcher: agent
3537
+ });
3538
+ if (response.status === 409) {
3539
+ logger.log(`Admin user "${username}" already configured.`);
3540
+ } else if (!response.ok) {
3541
+ const body = await response.text().catch(() => "");
3542
+ logger.log(`Warning: Could not configure admin "${username}" \u2014 ${response.status}${body ? `: ${body}` : ""}`);
3543
+ } else {
3544
+ logger.log(`Admin user "${username}" configured successfully.`);
3545
+ }
3546
+ } catch (err) {
3547
+ const error = err;
3548
+ const cause = error.cause instanceof Error ? ` (cause: ${error.cause.message})` : "";
3549
+ logger.log(`Warning: Could not reach gateway for admin setup \u2014 ${error.message}${cause}`);
3550
+ }
3551
+ }
3552
+ }
3553
+ function buildLayout(tmpDir, coreDirName) {
3554
+ const coreDir = join31(tmpDir, coreDirName);
3555
+ return {
3556
+ rootDir: tmpDir,
3557
+ coreDir,
3558
+ coreDirName,
3559
+ localDir: join31(coreDir, "local")
3560
+ };
3561
+ }
3562
+ async function generateProxyAppCompose(appDir, localDir, platformName, applicationName, logger) {
3563
+ const appComposePath = join31(appDir, "docker-compose.yml");
3564
+ let content;
3565
+ try {
3566
+ content = await readFile13(appComposePath, "utf-8");
3567
+ } catch {
3568
+ logger.log(`Warning: No docker-compose.yml found at ${appComposePath} \u2014 app services won't start.`);
3569
+ return;
3570
+ }
3571
+ const withAbsContexts = content.replace(
3572
+ /^(\s+context:\s+)\.\/(.*)$/gm,
3573
+ `$1${appDir}/$2`
3574
+ );
3575
+ const appParentDir = dirname13(appDir);
3576
+ const rewritten = withAbsContexts.replace(/\$\{PWD\}/g, appParentDir);
3577
+ const proxyPath = join31(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
3578
+ await writeFile11(proxyPath, rewritten, "utf-8");
3579
+ logger.log(`Generated app compose with absolute build paths.`);
3580
+ }
3581
+ function buildComposeManifest(manifest) {
3582
+ return {
3583
+ ...manifest,
3584
+ applications: manifest.applications.map((app) => ({ ...app, localPath: "." }))
3585
+ };
3586
+ }
3587
+ async function startStandalone(config, appDir, logger, signal, detach = false) {
3588
+ const { platformName, organizationName, applicationName, applicationDisplayName } = config;
3589
+ const platformTitle = camelize(platformName);
3590
+ const tmpDir = getTmpDir(platformName);
3591
+ const coreDirName = `${platformName}-core`;
3592
+ const bootstrapSuffix = "bootstrap-service";
3593
+ const customizationUiSuffix = "customization-ui";
3594
+ const bootstrapServiceName = `${platformName}-${bootstrapSuffix}`;
3595
+ const customizationUiName = `${platformName}-${customizationUiSuffix}`;
3596
+ const runningSingleton = await findRunningSingleton();
3597
+ if (runningSingleton) {
3598
+ logger.log(`Error: A standalone platform is already running at ${runningSingleton}.`);
3599
+ logger.log(`Run "platform standalone-stop" to stop it before starting a new one.`);
3600
+ return;
3601
+ }
3602
+ logger.log(`Starting standalone platform for "${applicationDisplayName}"...`);
3603
+ await mkdir5(tmpDir, { recursive: true });
3604
+ await writeFile11(join31(tmpDir, DETACH_MARKER), platformName, "utf-8");
3605
+ const variables = {
3606
+ organizationName,
3607
+ platformName,
3608
+ platformTitle,
3609
+ platformDisplayName: platformTitle,
3610
+ bootstrapName: platformName,
3611
+ bootstrapServiceName,
3612
+ customizationUiName,
3613
+ bootstrapServiceDir: `${coreDirName}/services`
3614
+ };
3615
+ logger.log("Scaffolding temporary platform core...");
3616
+ await scaffoldPlatform(tmpDir, variables, logger);
3617
+ await generateLocalEnv(tmpDir, logger, coreDirName);
3618
+ const npmManifest = addApplicationToManifest(
3619
+ createInitialManifest({ organizationName, platformName, platformDisplayName: platformTitle }),
3620
+ {
3621
+ name: applicationName,
3622
+ displayName: applicationDisplayName,
3623
+ description: "",
3624
+ localPath: appDir,
3625
+ repository: null
3626
+ }
3627
+ );
3628
+ await writeManifest(npmManifest, tmpDir, coreDirName);
3629
+ const bootstrapServiceDir = join31(tmpDir, coreDirName, "services", bootstrapServiceName);
3630
+ await scaffoldPlatformBootstrap(bootstrapServiceDir, variables, logger);
3631
+ const customizationUiDir = join31(tmpDir, coreDirName, "ui", customizationUiName);
3632
+ await scaffoldCustomizationUi(customizationUiDir, variables, logger);
3633
+ await registerCustomizationModule(bootstrapServiceDir, organizationName, logger, platformName);
3634
+ const layout = buildLayout(tmpDir, coreDirName);
3635
+ logger.log("Installing dependencies...");
3636
+ await installDependencies(layout, npmManifest, logger, signal, true);
3637
+ if (signal?.aborted) return;
3638
+ logger.log("Building...");
3639
+ await buildAll(layout, npmManifest, logger, signal, true);
3640
+ if (signal?.aborted) return;
3641
+ await generateProxyAppCompose(appDir, layout.localDir, platformName, applicationName, logger);
3642
+ const composeManifest = buildComposeManifest(npmManifest);
3643
+ await writeManifest(composeManifest, tmpDir, coreDirName);
3644
+ logger.log("Resetting previous environment (removing stale containers and volumes)...");
3645
+ await resetEnvironment(layout, composeManifest, logger, signal);
3646
+ if (signal?.aborted) return;
3647
+ logger.log("Starting containers...");
3648
+ await startEnvironment(layout, composeManifest, logger, signal);
3649
+ if (signal?.aborted) return;
3650
+ const env = await readEnvFile(join31(layout.localDir, ".env")).catch(() => /* @__PURE__ */ new Map());
3651
+ const gatewayUrl = env.get("PAE_GATEWAY_HOST_URL") ?? "https://localhost:443";
3652
+ const gatewayReady = await waitForGateway(gatewayUrl, logger);
3653
+ if (!gatewayReady) {
3654
+ logger.log("Error: Gateway did not become healthy. The platform may not be functioning correctly.");
3655
+ logger.log(`Check container logs with: docker compose -p ${platformName}-platform logs pae-nestjs-gateway-service`);
3656
+ }
3657
+ if (gatewayReady) {
3658
+ await configureIdpFromConfig(config, layout.localDir, logger);
3659
+ await configureAdminUsersFromConfig(config, layout.localDir, logger);
3660
+ }
3661
+ if (detach) {
3662
+ logger.log(`
3663
+ Standalone platform is running in the background.`);
3664
+ logger.log(`Run "platform standalone-stop" to stop and clean up.`);
3665
+ return;
3666
+ }
3667
+ logger.log("\nPlatform is running. Press Ctrl+C to stop and clean up.");
3668
+ const keepAlive = setInterval(() => {
3669
+ }, 60 * 6e4);
3670
+ const shutdown = () => {
3671
+ clearInterval(keepAlive);
3672
+ void (async () => {
3673
+ logger.log("\nShutting down standalone platform...");
3674
+ await destroyEnvironment(layout, composeManifest, logger);
3675
+ await cleanupTmpDir(tmpDir, logger);
3676
+ })();
3677
+ };
3678
+ process.once("SIGINT", shutdown);
3679
+ process.once("SIGTERM", shutdown);
3680
+ }
3681
+ async function stopStandalone(logger) {
3682
+ const runningDir = await findRunningSingleton();
3683
+ if (!runningDir) {
3684
+ logger.log("No detached standalone platform found. Nothing to stop.");
3685
+ return;
3686
+ }
3687
+ let coreDirName = null;
3688
+ try {
3689
+ const entries = await readdir7(runningDir, { withFileTypes: true });
3690
+ for (const entry of entries) {
3691
+ if (entry.isDirectory() && entry.name.endsWith("-core")) {
3692
+ try {
3693
+ await access10(join31(runningDir, entry.name, "product.manifest.json"));
3694
+ coreDirName = entry.name;
3695
+ break;
3696
+ } catch {
3697
+ }
3698
+ }
3699
+ }
3700
+ } catch {
3701
+ }
3702
+ if (!coreDirName) {
3703
+ logger.log("Error: Could not find platform core in standalone directory. Attempting raw cleanup...");
3704
+ await cleanupTmpDir(runningDir, logger);
3705
+ return;
3706
+ }
3707
+ const layout = buildLayout(runningDir, coreDirName);
3708
+ let manifest;
3709
+ try {
3710
+ manifest = await readManifest(runningDir, coreDirName);
3711
+ } catch {
3712
+ logger.log("Error: Could not read manifest. Attempting raw cleanup...");
3713
+ const platformName = coreDirName.replace("-core", "");
3714
+ await destroyEnvironment(
3715
+ layout,
3716
+ { version: "1", product: { name: platformName, displayName: "", organization: "", scope: "" }, applications: [] },
3717
+ logger
3718
+ );
3719
+ await cleanupTmpDir(runningDir, logger);
3720
+ return;
3721
+ }
3722
+ logger.log("Stopping standalone platform...");
3723
+ await destroyEnvironment(layout, manifest, logger);
3724
+ await cleanupTmpDir(runningDir, logger);
3725
+ logger.log("Standalone platform stopped and cleaned up.");
3726
+ }
3727
+
3728
+ // src/controllers/ui/standalone.ui-controller.ts
3729
+ function getGitEmail() {
3730
+ return new Promise((resolve9) => {
3731
+ exec("git config user.email", (err, stdout) => {
3732
+ resolve9(err ? "" : stdout.trim());
3733
+ });
3734
+ });
3735
+ }
3736
+ async function collectAdminUsers(ctx) {
3737
+ ctx.log("\nAdd platform admin users (users who can access the admin UI).");
3738
+ const defaultEmail = await getGitEmail();
3739
+ const adminUsers = [];
3740
+ let addMore = true;
3741
+ while (addMore) {
3742
+ const suggestion = adminUsers.length === 0 ? defaultEmail : "";
3743
+ const username = await ctx.prompt("Admin user (email/username):", suggestion);
3744
+ if (username.trim()) {
3745
+ adminUsers.push(username.trim());
3746
+ }
3747
+ addMore = await ctx.confirm("Add another admin user?", false);
3748
+ }
3749
+ return adminUsers;
3750
+ }
3751
+ async function collectIdpConfig(ctx) {
3752
+ const providers2 = getAllProviders();
3753
+ const providerType = await ctx.select(
3754
+ "Select IDP provider:",
3755
+ providers2.map((p) => ({ label: p.displayName, value: p.type }))
3756
+ );
3757
+ const provider = providers2.find((p) => p.type === providerType);
3758
+ const name = await ctx.prompt("Provider name:");
3759
+ const issuer = await ctx.prompt("Issuer URL:");
3760
+ const clientId = await ctx.prompt("Client ID:");
3761
+ const clientSecret = await ctx.prompt("Client Secret:");
3762
+ const extras = {};
3763
+ for (const field of provider.fields) {
3764
+ extras[field.key] = await ctx.prompt(field.prompt);
3765
+ }
3766
+ return { provider: providerType, name, issuer, clientId, clientSecret, extras };
3767
+ }
3768
+ async function standaloneUiController(ctx) {
3769
+ const appDir = cwd7();
3770
+ const layout = await findAppMonorepoLayout(appDir);
3771
+ if (!layout) {
3772
+ ctx.log("Error: Not in an application monorepo. Run this command from the root of an application folder (must have docker-compose.yml and package.json).");
3773
+ return;
3774
+ }
3775
+ const detected = await autoDetectAppIdentity(appDir);
3776
+ const defaults = await getDefaults();
3777
+ let config = null;
3778
+ if (detected) {
3779
+ const pn = detected.platformName;
3780
+ const appName = detected.applicationName;
3781
+ config = await resolveAppConfig(pn, appName);
3782
+ }
3783
+ if (!config) {
3784
+ const legacy = await readLegacyConfig(appDir);
3785
+ if (legacy) {
3786
+ ctx.log("Found legacy standalone.json in this directory. Migrating to centralized config...");
3787
+ const appKey = buildAppKey(legacy.platformName, legacy.applicationName);
3788
+ const defaultsUpdate = {};
3789
+ if (!defaults.platformName) defaultsUpdate.platformName = legacy.platformName;
3790
+ if (!defaults.organizationName) defaultsUpdate.organizationName = legacy.organizationName;
3791
+ if (!defaults.idp && legacy.idp) defaultsUpdate.idp = legacy.idp;
3792
+ if (!defaults.adminUsers && legacy.adminUsers) defaultsUpdate.adminUsers = legacy.adminUsers;
3793
+ if (Object.keys(defaultsUpdate).length > 0) {
3794
+ await saveDefaults(defaultsUpdate);
3795
+ }
3796
+ await saveAppConfig(appKey, {
3797
+ applicationName: legacy.applicationName,
3798
+ applicationDisplayName: legacy.applicationDisplayName,
3799
+ // Save IDP/admins as app override only if different from what we put in defaults
3800
+ ...legacy.idp && defaults.idp ? { idp: legacy.idp } : {},
3801
+ ...legacy.adminUsers && defaults.adminUsers ? { adminUsers: legacy.adminUsers } : {}
3802
+ });
3803
+ config = await resolveAppConfig(legacy.platformName, legacy.applicationName);
3804
+ ctx.log(`Migrated config to centralized cache (~/.ba-platform/standalone.json).`);
3805
+ const deleteLegacy = await ctx.confirm("Delete the local standalone.json?", true);
3806
+ if (deleteLegacy) {
3807
+ try {
3808
+ await rm3(join32(appDir, "standalone.json"));
3809
+ ctx.log("Deleted standalone.json.");
3810
+ } catch {
3811
+ ctx.log("Warning: Could not delete standalone.json.");
3812
+ }
3813
+ }
3814
+ }
3815
+ }
3816
+ if (!config) {
3817
+ ctx.log("No standalone config found. Let's set it up.\n");
3818
+ const defaultPlatformName = detected?.platformName ?? defaults.platformName ?? "";
3819
+ const platformName = await ctx.prompt("Platform name (e.g. myplatform):", defaultPlatformName);
3820
+ if (!/^[a-z0-9-]+$/.test(platformName)) {
3821
+ ctx.log("Error: Platform name must use only lowercase letters, numbers, and hyphens.");
3822
+ return;
3823
+ }
3824
+ const defaultOrgName = defaults.organizationName ?? "";
3825
+ const organizationName = await ctx.prompt("Organization name (npm scope, e.g. myorg):", defaultOrgName);
3826
+ if (!/^[a-z0-9-]+$/.test(organizationName)) {
3827
+ ctx.log("Error: Organization name must use only lowercase letters, numbers, and hyphens.");
3828
+ return;
3829
+ }
3830
+ const defaultAppName = detected?.applicationName ?? "";
3831
+ const applicationName = await ctx.prompt("Application name:", defaultAppName);
3832
+ if (!/^[a-z0-9-]+$/.test(applicationName)) {
3833
+ ctx.log("Error: Application name must use only lowercase letters, numbers, and hyphens.");
3834
+ return;
3835
+ }
3836
+ const defaultDisplayName = detected?.applicationDisplayName ?? applicationName;
3837
+ const applicationDisplayName = await ctx.prompt("Application display name:", defaultDisplayName);
3838
+ let idp;
3839
+ let idpIsDefault = false;
3840
+ if (defaults.idp) {
3841
+ const useDefaultIdp = await ctx.confirm(`Use default IDP (${defaults.idp.provider}: ${defaults.idp.name})?`, true);
3842
+ if (useDefaultIdp) {
3843
+ idp = void 0;
3844
+ idpIsDefault = true;
3845
+ } else {
3846
+ ctx.log("\nConfigure a new IDP for this application.");
3847
+ idp = await collectIdpConfig(ctx);
3848
+ }
3849
+ } else {
3850
+ ctx.log("\nAn Identity Provider (IDP) is required to log into the platform.");
3851
+ idp = await collectIdpConfig(ctx);
3852
+ }
3853
+ let adminUsers;
3854
+ let adminsAreDefault = false;
3855
+ if (defaults.adminUsers && defaults.adminUsers.length > 0) {
3856
+ const useDefaultAdmins = await ctx.confirm(`Use default admin users (${defaults.adminUsers.join(", ")})?`, true);
3857
+ if (useDefaultAdmins) {
3858
+ adminUsers = void 0;
3859
+ adminsAreDefault = true;
3860
+ } else {
3861
+ adminUsers = await collectAdminUsers(ctx);
3862
+ }
3863
+ } else {
3864
+ adminUsers = await collectAdminUsers(ctx);
3865
+ }
3866
+ const defaultsUpdate = {};
3867
+ if (!defaults.platformName) defaultsUpdate.platformName = platformName;
3868
+ if (!defaults.organizationName) defaultsUpdate.organizationName = organizationName;
3869
+ if (!defaults.idp && idp && !idpIsDefault) defaultsUpdate.idp = idp;
3870
+ if (!defaults.adminUsers && adminUsers && !adminsAreDefault) defaultsUpdate.adminUsers = adminUsers;
3871
+ if (Object.keys(defaultsUpdate).length > 0) {
3872
+ await saveDefaults(defaultsUpdate);
3873
+ }
3874
+ const appKey = buildAppKey(platformName, applicationName);
3875
+ await saveAppConfig(appKey, {
3876
+ applicationName,
3877
+ applicationDisplayName,
3878
+ // Only store overrides if different from defaults
3879
+ ...platformName !== defaults.platformName && defaults.platformName ? { platformName } : {},
3880
+ ...organizationName !== defaults.organizationName && defaults.organizationName ? { organizationName } : {},
3881
+ ...idp && !idpIsDefault ? { idp } : {},
3882
+ ...adminUsers && !adminsAreDefault ? { adminUsers } : {}
3883
+ });
3884
+ config = await resolveAppConfig(platformName, applicationName);
3885
+ ctx.log("Saved standalone config.");
3886
+ }
3887
+ if (config && !config.idp) {
3888
+ ctx.log("IDP configuration is required but not found.");
3889
+ const idp = await collectIdpConfig(ctx);
3890
+ if (!defaults.idp) {
3891
+ await saveDefaults({ idp });
3892
+ } else {
3893
+ const appKey = buildAppKey(config.platformName, config.applicationName);
3894
+ const cache = await readCacheFile();
3895
+ const existing = cache.apps[appKey];
3896
+ if (existing) {
3897
+ await saveAppConfig(appKey, { ...existing, idp });
3898
+ }
3899
+ }
3900
+ config = await resolveAppConfig(config.platformName, config.applicationName);
3901
+ ctx.log("Saved IDP configuration.");
3902
+ }
3903
+ if (config && (!config.adminUsers || config.adminUsers.length === 0)) {
3904
+ ctx.log("No admin users configured.");
3905
+ const adminUsers = await collectAdminUsers(ctx);
3906
+ if (!defaults.adminUsers || defaults.adminUsers.length === 0) {
3907
+ await saveDefaults({ adminUsers });
3908
+ } else {
3909
+ const appKey = buildAppKey(config.platformName, config.applicationName);
3910
+ const cache = await readCacheFile();
3911
+ const existing = cache.apps[appKey];
3912
+ if (existing) {
3913
+ await saveAppConfig(appKey, { ...existing, adminUsers });
3914
+ }
3915
+ }
3916
+ config = await resolveAppConfig(config.platformName, config.applicationName);
3917
+ ctx.log("Saved admin users.");
3918
+ }
3919
+ if (!config) {
3920
+ ctx.log("Error: Could not resolve standalone config.");
3921
+ return;
3922
+ }
3923
+ const detach = await ctx.confirm("Run in detached mode (background)?", false);
3924
+ await startStandalone(config, appDir, ctx, ctx.signal, detach);
3925
+ }
3926
+ async function standaloneStopUiController(ctx) {
3927
+ await stopStandalone(ctx);
3928
+ }
3929
+
3930
+ // src/controllers/ui/standalone-config.ui-controller.ts
3931
+ import { cwd as cwd8 } from "process";
3932
+ import { exec as exec2 } from "child_process";
3933
+ async function standaloneConfigShowUiController(ctx) {
3934
+ const detected = await autoDetectAppIdentity(cwd8());
3935
+ if (detected) {
3936
+ const config = await resolveAppConfig(detected.platformName, detected.applicationName);
3937
+ if (config) {
3938
+ ctx.log(`Standalone config for ${detected.platformName}/${detected.applicationName}:
3939
+ `);
3940
+ ctx.log(JSON.stringify(config, null, 2));
3941
+ } else {
3942
+ ctx.log(`No config found for ${detected.platformName}/${detected.applicationName}.`);
3943
+ ctx.log(`Run 'platform standalone' or 'platform standalone-config set' to configure.`);
3944
+ }
3945
+ } else {
3946
+ await showAll(ctx);
3947
+ }
3948
+ }
3949
+ async function showAll(ctx) {
3950
+ const cache = await readCacheFile();
3951
+ const defaults = cache.defaults;
3952
+ const apps = await listAppConfigs();
3953
+ ctx.log(`Config file: ${getCachePath()}
3954
+ `);
3955
+ ctx.log("--- Defaults ---");
3956
+ if (Object.keys(defaults).length === 0) {
3957
+ ctx.log("(none)");
3958
+ } else {
3959
+ ctx.log(JSON.stringify(defaults, null, 2));
3960
+ }
3961
+ ctx.log("\n--- Apps ---");
3962
+ if (apps.length === 0) {
3963
+ ctx.log("(none)");
3964
+ } else {
3965
+ for (const { key, resolved } of apps) {
3966
+ ctx.log(`
3967
+ ${key}:`);
3968
+ ctx.log(` Platform: ${resolved.platformName}`);
3969
+ ctx.log(` Organization: ${resolved.organizationName}`);
3970
+ ctx.log(` Display Name: ${resolved.applicationDisplayName}`);
3971
+ ctx.log(` IDP: ${resolved.idp ? `${resolved.idp.provider} (${resolved.idp.name})` : "(not configured)"}`);
3972
+ ctx.log(` Admin Users: ${resolved.adminUsers?.join(", ") ?? "(not configured)"}`);
3973
+ }
3974
+ }
3975
+ }
3976
+ async function standaloneConfigSetUiController(ctx) {
3977
+ const scope = await ctx.select("What do you want to edit?", [
3978
+ { label: "Defaults (shared across all apps)", value: "defaults" },
3979
+ { label: "App-specific config", value: "app" }
3980
+ ]);
3981
+ if (scope === "defaults") {
3982
+ await editDefaults(ctx);
3983
+ } else {
3984
+ await editApp(ctx);
3985
+ }
3986
+ }
3987
+ async function editDefaults(ctx) {
3988
+ const defaults = await getDefaults();
3989
+ const platformName = await ctx.prompt("Platform name:", defaults.platformName ?? "");
3990
+ const organizationName = await ctx.prompt("Organization name:", defaults.organizationName ?? "");
3991
+ const update = {};
3992
+ if (platformName) update.platformName = platformName;
3993
+ if (organizationName) update.organizationName = organizationName;
3994
+ const editIdp = defaults.idp ? await ctx.confirm(`Edit IDP (currently: ${defaults.idp.provider} - ${defaults.idp.name})?`, false) : await ctx.confirm("Configure an IDP?", true);
3995
+ if (editIdp) {
3996
+ update.idp = await collectIdpConfig2(ctx, defaults.idp);
3997
+ }
3998
+ const editAdmins = defaults.adminUsers?.length ? await ctx.confirm(`Edit admin users (currently: ${defaults.adminUsers.join(", ")})?`, false) : await ctx.confirm("Configure admin users?", true);
3999
+ if (editAdmins) {
4000
+ update.adminUsers = await collectAdminUsers2(ctx);
4001
+ }
4002
+ await saveDefaults(update);
4003
+ ctx.log("Defaults saved.");
4004
+ }
4005
+ async function editApp(ctx) {
4006
+ const apps = await listAppConfigs();
4007
+ const detected = await autoDetectAppIdentity(cwd8());
4008
+ let appKey;
4009
+ if (apps.length > 0) {
4010
+ const options = apps.map(({ key }) => ({ label: key, value: key }));
4011
+ if (detected) {
4012
+ const detectedKey = buildAppKey(detected.platformName, detected.applicationName);
4013
+ if (!options.find((o) => o.value === detectedKey)) {
4014
+ options.unshift({ label: `${detectedKey} (new - detected)`, value: detectedKey });
4015
+ }
4016
+ }
4017
+ options.push({ label: "Add new app...", value: "__new__" });
4018
+ appKey = await ctx.select("Select app:", options);
4019
+ } else if (detected) {
4020
+ appKey = buildAppKey(detected.platformName, detected.applicationName);
4021
+ ctx.log(`Detected app: ${appKey}`);
4022
+ } else {
4023
+ appKey = "__new__";
4024
+ }
4025
+ if (appKey === "__new__") {
4026
+ const pn = await ctx.prompt("Platform name:");
4027
+ const an = await ctx.prompt("Application name:");
4028
+ appKey = buildAppKey(pn, an);
4029
+ }
4030
+ const cache = await readCacheFile();
4031
+ const existing = cache.apps[appKey];
4032
+ const applicationName = await ctx.prompt("Application name:", existing?.applicationName ?? appKey.split("/")[1] ?? "");
4033
+ const applicationDisplayName = await ctx.prompt("Application display name:", existing?.applicationDisplayName ?? applicationName);
4034
+ const overrideIdp = await ctx.confirm("Set app-specific IDP (override defaults)?", !!existing?.idp);
4035
+ let idp;
4036
+ if (overrideIdp) {
4037
+ idp = await collectIdpConfig2(ctx, existing?.idp);
4038
+ }
4039
+ const overrideAdmins = await ctx.confirm("Set app-specific admin users (override defaults)?", !!existing?.adminUsers);
4040
+ let adminUsers;
4041
+ if (overrideAdmins) {
4042
+ adminUsers = await collectAdminUsers2(ctx);
4043
+ }
4044
+ await saveAppConfig(appKey, {
4045
+ applicationName,
4046
+ applicationDisplayName,
4047
+ ...idp ? { idp } : {},
4048
+ ...adminUsers ? { adminUsers } : {}
4049
+ });
4050
+ ctx.log(`App config for "${appKey}" saved.`);
4051
+ }
4052
+ async function standaloneConfigDeleteUiController(ctx) {
4053
+ const apps = await listAppConfigs();
4054
+ if (apps.length === 0) {
4055
+ ctx.log("No app configs to delete.");
4056
+ return;
4057
+ }
4058
+ const appKey = await ctx.select(
4059
+ "Select app config to delete:",
4060
+ apps.map(({ key }) => ({ label: key, value: key }))
4061
+ );
4062
+ const confirmed = await ctx.confirm(`Delete config for "${appKey}"?`, false);
4063
+ if (!confirmed) {
4064
+ ctx.log("Cancelled.");
4065
+ return;
4066
+ }
4067
+ const deleted = await deleteAppConfig(appKey);
4068
+ ctx.log(deleted ? `Deleted config for "${appKey}".` : `Config for "${appKey}" not found.`);
4069
+ }
4070
+ async function standaloneConfigListUiController(ctx) {
4071
+ await showAll(ctx);
4072
+ }
4073
+ async function collectIdpConfig2(ctx, current) {
4074
+ const providers2 = getAllProviders();
4075
+ const providerType = await ctx.select(
4076
+ "Select IDP provider:",
4077
+ providers2.map((p) => ({ label: p.displayName, value: p.type }))
4078
+ );
4079
+ const provider = providers2.find((p) => p.type === providerType);
4080
+ const name = await ctx.prompt("Provider name:", current?.name ?? "");
4081
+ const issuer = await ctx.prompt("Issuer URL:", current?.issuer ?? "");
4082
+ const clientId = await ctx.prompt("Client ID:", current?.clientId ?? "");
4083
+ const clientSecret = await ctx.prompt("Client Secret:", current?.clientSecret ?? "");
4084
+ const extras = {};
4085
+ for (const field of provider.fields) {
4086
+ extras[field.key] = await ctx.prompt(field.prompt, current?.extras?.[field.key] ?? "");
4087
+ }
4088
+ return { provider: providerType, name, issuer, clientId, clientSecret, extras };
4089
+ }
4090
+ function getGitEmail2() {
4091
+ return new Promise((resolve9) => {
4092
+ exec2("git config user.email", (err, stdout) => {
4093
+ resolve9(err ? "" : stdout.trim());
4094
+ });
4095
+ });
4096
+ }
4097
+ async function collectAdminUsers2(ctx) {
4098
+ const defaultEmail = await getGitEmail2();
4099
+ const adminUsers = [];
4100
+ let addMore = true;
4101
+ while (addMore) {
4102
+ const suggestion = adminUsers.length === 0 ? defaultEmail : "";
4103
+ const username = await ctx.prompt("Admin user (email/username):", suggestion);
4104
+ if (username.trim()) {
4105
+ adminUsers.push(username.trim());
4106
+ }
4107
+ addMore = await ctx.confirm("Add another admin user?", false);
4108
+ }
4109
+ return adminUsers;
4110
+ }
4111
+
3057
4112
  // src/controllers/ui/registry.ts
3058
4113
  var uiControllers = /* @__PURE__ */ new Map([
3059
4114
  [CREATE_APPLICATION_COMMAND_NAME, createApplicationUiController],
@@ -3068,7 +4123,13 @@ var uiControllers = /* @__PURE__ */ new Map([
3068
4123
  [STOP_COMMAND_NAME, createLocalScriptUiController(STOP_COMMAND_NAME)],
3069
4124
  [DESTROY_COMMAND_NAME, createLocalScriptUiController(DESTROY_COMMAND_NAME)],
3070
4125
  [MANAGE_PLATFORM_ADMINS_COMMAND_NAME, managePlatformAdminsUiController],
3071
- [INSTALL_AI_PLUGIN_COMMAND_NAME, installAiPluginUiController]
4126
+ [INSTALL_AI_PLUGIN_COMMAND_NAME, installAiPluginUiController],
4127
+ [STANDALONE_COMMAND_NAME, standaloneUiController],
4128
+ [STANDALONE_STOP_COMMAND_NAME, standaloneStopUiController],
4129
+ [STANDALONE_CONFIG_SHOW_COMMAND_NAME, standaloneConfigShowUiController],
4130
+ [STANDALONE_CONFIG_SET_COMMAND_NAME, standaloneConfigSetUiController],
4131
+ [STANDALONE_CONFIG_DELETE_COMMAND_NAME, standaloneConfigDeleteUiController],
4132
+ [STANDALONE_CONFIG_LIST_COMMAND_NAME, standaloneConfigListUiController]
3072
4133
  ]);
3073
4134
 
3074
4135
  // src/hooks/use-command-runner.ts
@@ -3102,7 +4163,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
3102
4163
  }
3103
4164
  },
3104
4165
  prompt(message, defaultValue) {
3105
- return new Promise((resolve6, reject) => {
4166
+ return new Promise((resolve9, reject) => {
3106
4167
  if (controller.signal.aborted) {
3107
4168
  reject(new DOMException("Aborted", "AbortError"));
3108
4169
  return;
@@ -3110,7 +4171,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
3110
4171
  setPromptMessage(message);
3111
4172
  setPromptValue(defaultValue ?? "");
3112
4173
  setPromptMode({ kind: "text" });
3113
- promptResolveRef.current = resolve6;
4174
+ promptResolveRef.current = resolve9;
3114
4175
  setState(APP_STATE.PROMPTING);
3115
4176
  controller.signal.addEventListener(
3116
4177
  "abort",
@@ -3120,7 +4181,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
3120
4181
  });
3121
4182
  },
3122
4183
  select(message, options) {
3123
- return new Promise((resolve6, reject) => {
4184
+ return new Promise((resolve9, reject) => {
3124
4185
  if (controller.signal.aborted) {
3125
4186
  reject(new DOMException("Aborted", "AbortError"));
3126
4187
  return;
@@ -3128,7 +4189,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
3128
4189
  setPromptMessage(message);
3129
4190
  setPromptMode({ kind: "select", options });
3130
4191
  setSelectIndex(0);
3131
- promptResolveRef.current = resolve6;
4192
+ promptResolveRef.current = resolve9;
3132
4193
  setState(APP_STATE.PROMPTING);
3133
4194
  controller.signal.addEventListener(
3134
4195
  "abort",
@@ -3138,7 +4199,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
3138
4199
  });
3139
4200
  },
3140
4201
  multiselect(message, options) {
3141
- return new Promise((resolve6, reject) => {
4202
+ return new Promise((resolve9, reject) => {
3142
4203
  if (controller.signal.aborted) {
3143
4204
  reject(new DOMException("Aborted", "AbortError"));
3144
4205
  return;
@@ -3148,7 +4209,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
3148
4209
  setSelectIndex(0);
3149
4210
  setMultiselectChecked(new Set(options.map((_, i) => i)));
3150
4211
  promptResolveRef.current = (value) => {
3151
- resolve6(value ? value.split(",") : []);
4212
+ resolve9(value ? value.split(",") : []);
3152
4213
  };
3153
4214
  setState(APP_STATE.PROMPTING);
3154
4215
  controller.signal.addEventListener(
@@ -3159,7 +4220,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
3159
4220
  });
3160
4221
  },
3161
4222
  confirm(message, defaultValue) {
3162
- return new Promise((resolve6, reject) => {
4223
+ return new Promise((resolve9, reject) => {
3163
4224
  if (controller.signal.aborted) {
3164
4225
  reject(new DOMException("Aborted", "AbortError"));
3165
4226
  return;
@@ -3167,7 +4228,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
3167
4228
  setPromptMessage(message);
3168
4229
  setPromptMode({ kind: "confirm" });
3169
4230
  setConfirmValue(defaultValue ?? true);
3170
- promptResolveRef.current = (value) => resolve6(value === "true");
4231
+ promptResolveRef.current = (value) => resolve9(value === "true");
3171
4232
  setState(APP_STATE.PROMPTING);
3172
4233
  controller.signal.addEventListener(
3173
4234
  "abort",
@@ -3201,11 +4262,11 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
3201
4262
  );
3202
4263
  const handlePromptSubmit = useCallback(
3203
4264
  (value) => {
3204
- const resolve6 = promptResolveRef.current;
4265
+ const resolve9 = promptResolveRef.current;
3205
4266
  promptResolveRef.current = null;
3206
4267
  setPromptMode({ kind: "text" });
3207
4268
  setState(APP_STATE.EXECUTING);
3208
- resolve6?.(value);
4269
+ resolve9?.(value);
3209
4270
  },
3210
4271
  [setState]
3211
4272
  );
@@ -3237,9 +4298,12 @@ function App() {
3237
4298
  const [selectedIndex, setSelectedIndex] = useState3(0);
3238
4299
  const [staticItems, setStaticItems] = useState3([{ kind: "banner", id: "banner" }]);
3239
4300
  const [platformInitialized, setPlatformInitialized] = useState3(false);
4301
+ const [inAppMonorepo, setInAppMonorepo] = useState3(false);
3240
4302
  useEffect2(() => {
3241
4303
  isPlatformInitialized().then(setPlatformInitialized).catch(() => {
3242
4304
  });
4305
+ isInAppMonorepo().then(setInAppMonorepo).catch(() => {
4306
+ });
3243
4307
  }, []);
3244
4308
  const appendStaticItem = useCallback2((item) => {
3245
4309
  setStaticItems((prev) => [...prev, item]);
@@ -3247,6 +4311,8 @@ function App() {
3247
4311
  const handleCommandComplete = useCallback2(() => {
3248
4312
  isPlatformInitialized().then(setPlatformInitialized).catch(() => {
3249
4313
  });
4314
+ isInAppMonorepo().then(setInAppMonorepo).catch(() => {
4315
+ });
3250
4316
  }, []);
3251
4317
  const {
3252
4318
  runCommand: runCommand2,
@@ -3264,7 +4330,7 @@ function App() {
3264
4330
  setMultiselectChecked
3265
4331
  } = useCommandRunner({ appendStaticItem, setState, onCommandComplete: handleCommandComplete });
3266
4332
  const query = inputValue.startsWith("/") ? inputValue.slice(1) : "";
3267
- const filteredCommands = registry.searchVisible(query, { platformInitialized });
4333
+ const filteredCommands = registry.searchVisible(query, { platformInitialized, inAppMonorepo });
3268
4334
  useInput(
3269
4335
  (input, key) => {
3270
4336
  if (key.ctrl && input === "c") {
@@ -3446,32 +4512,39 @@ function App() {
3446
4512
 
3447
4513
  // src/controllers/cli/create-application.cli-controller.ts
3448
4514
  async function createApplicationCliController(args2) {
3449
- if (!await isPlatformInitialized()) {
3450
- console.error("Error: Cannot create an application \u2014 no platform initialized in this directory.");
3451
- process.exit(1);
3452
- }
4515
+ const insidePlatform = await isPlatformInitialized();
3453
4516
  const {
3454
4517
  applicationName,
3455
4518
  applicationDisplayName,
3456
4519
  applicationDescription,
3457
4520
  hasUserInterface,
3458
- hasBackendService
4521
+ hasBackendService,
4522
+ platformName,
4523
+ organizationName
3459
4524
  } = args2;
3460
- if (!applicationName || !applicationDisplayName || !applicationDescription) {
3461
- console.error("Error: applicationName, applicationDisplayName, and applicationDescription are required.");
4525
+ if (!applicationName) {
4526
+ console.error("Error: applicationName is required.");
3462
4527
  process.exit(1);
3463
4528
  }
3464
4529
  if (!/^[a-z0-9-]+$/.test(applicationName)) {
3465
4530
  console.error(`Error: Application name "${applicationName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
3466
4531
  process.exit(1);
3467
4532
  }
4533
+ if (!insidePlatform) {
4534
+ if (!platformName || !organizationName) {
4535
+ console.error("Error: platformName and organizationName are required when creating an application outside a platform.");
4536
+ process.exit(1);
4537
+ }
4538
+ }
3468
4539
  await createApplicationService(
3469
4540
  {
3470
4541
  applicationName,
3471
- applicationDisplayName,
3472
- applicationDescription,
3473
- hasUserInterface: hasUserInterface === "yes",
3474
- hasBackendService: hasBackendService === "yes"
4542
+ applicationDisplayName: applicationDisplayName || applicationName,
4543
+ applicationDescription: applicationDescription || "",
4544
+ hasUserInterface: insidePlatform ? hasUserInterface === "yes" : hasUserInterface !== "no",
4545
+ hasBackendService: insidePlatform ? hasBackendService === "yes" : hasBackendService !== "no",
4546
+ platformName,
4547
+ organizationName
3475
4548
  },
3476
4549
  { log: console.log }
3477
4550
  );
@@ -3725,6 +4798,160 @@ async function installAiPluginCliController(args2) {
3725
4798
  );
3726
4799
  }
3727
4800
 
4801
+ // src/controllers/cli/standalone.cli-controller.ts
4802
+ import { cwd as cwd9 } from "process";
4803
+ async function standaloneCliController(args2) {
4804
+ const logger = { log: console.log };
4805
+ const appDir = cwd9();
4806
+ const layout = await findAppMonorepoLayout(appDir);
4807
+ if (!layout) {
4808
+ console.error("Error: Not in an application monorepo. Run this command from the root of an application folder (must have docker-compose.yml and package.json).");
4809
+ process.exit(1);
4810
+ }
4811
+ const detected = await autoDetectAppIdentity(appDir);
4812
+ if (!detected) {
4813
+ console.error("Error: Could not auto-detect app identity. Run 'platform standalone' interactively or 'platform standalone-config set' to configure.");
4814
+ process.exit(1);
4815
+ }
4816
+ let config = await resolveAppConfig(detected.platformName, detected.applicationName);
4817
+ if (!config) {
4818
+ const defaults = await getDefaults();
4819
+ const appOverride = {
4820
+ applicationName: detected.applicationName,
4821
+ applicationDisplayName: detected.applicationDisplayName
4822
+ };
4823
+ config = mergeConfig(defaults, appOverride);
4824
+ if (!config.platformName) config.platformName = detected.platformName;
4825
+ if (!config.organizationName) config.organizationName = defaults.organizationName ?? "";
4826
+ if (!defaults.platformName) {
4827
+ await saveDefaults({ platformName: detected.platformName });
4828
+ }
4829
+ const appKey = buildAppKey(config.platformName, config.applicationName);
4830
+ await saveAppConfig(appKey, appOverride);
4831
+ logger.log(`Auto-configured standalone for ${appKey} (saved to ~/.ba-platform/standalone.json).`);
4832
+ }
4833
+ if (!config.idp) {
4834
+ console.error("Warning: No IDP configured. Users won't be able to log in. Run 'platform standalone-config set' to add an IDP.");
4835
+ }
4836
+ if (!config.adminUsers || config.adminUsers.length === 0) {
4837
+ console.error("Warning: No admin users configured. Run 'platform standalone-config set' to add admin users.");
4838
+ }
4839
+ let positional = [];
4840
+ try {
4841
+ positional = args2["_positional"] ? JSON.parse(args2["_positional"]) : [];
4842
+ } catch {
4843
+ }
4844
+ const detach = args2["detach"] === "true" || args2["d"] === "true" || positional.includes("--detach") || positional.includes("-d");
4845
+ await startStandalone(config, appDir, logger, void 0, detach);
4846
+ }
4847
+ async function standaloneStopCliController(_args) {
4848
+ const logger = { log: console.log };
4849
+ await stopStandalone(logger);
4850
+ }
4851
+
4852
+ // src/controllers/cli/standalone-config.cli-controller.ts
4853
+ import { cwd as cwd10 } from "process";
4854
+ async function standaloneConfigShowCliController(args2) {
4855
+ const appArg = args2["app"];
4856
+ if (appArg) {
4857
+ const parts = appArg.split("/");
4858
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
4859
+ console.error("Error: --app must be in format platformName/applicationName (exactly one slash).");
4860
+ process.exit(1);
4861
+ }
4862
+ const [pn, an] = parts;
4863
+ const config = await resolveAppConfig(pn, an);
4864
+ if (config) {
4865
+ console.log(JSON.stringify(config, null, 2));
4866
+ } else {
4867
+ console.error(`No config found for ${appArg}.`);
4868
+ process.exit(1);
4869
+ }
4870
+ return;
4871
+ }
4872
+ const detected = await autoDetectAppIdentity(cwd10());
4873
+ if (detected) {
4874
+ const config = await resolveAppConfig(detected.platformName, detected.applicationName);
4875
+ if (config) {
4876
+ console.log(JSON.stringify(config, null, 2));
4877
+ return;
4878
+ }
4879
+ }
4880
+ const cache = await readCacheFile();
4881
+ console.log(JSON.stringify(cache, null, 2));
4882
+ }
4883
+ async function standaloneConfigSetCliController(args2) {
4884
+ if (args2["defaults"] === "true") {
4885
+ const update = {};
4886
+ if (args2["platformName"]) update.platformName = args2["platformName"];
4887
+ if (args2["organizationName"]) update.organizationName = args2["organizationName"];
4888
+ if (Object.keys(update).length === 0) {
4889
+ console.error("Error: No fields provided. Use key=value pairs (e.g. platformName=myplatform organizationName=myorg).");
4890
+ process.exit(1);
4891
+ }
4892
+ await saveDefaults(update);
4893
+ console.log("Defaults updated.");
4894
+ return;
4895
+ }
4896
+ let appKey = args2["app"];
4897
+ if (!appKey) {
4898
+ const detected = await autoDetectAppIdentity(cwd10());
4899
+ if (detected) {
4900
+ appKey = buildAppKey(detected.platformName, detected.applicationName);
4901
+ } else {
4902
+ console.error("Error: Could not detect app. Provide --app=platformName/applicationName.");
4903
+ process.exit(1);
4904
+ }
4905
+ }
4906
+ const cache = await readCacheFile();
4907
+ const existing = cache.apps[appKey] ?? { applicationName: appKey.split("/")[1] ?? "", applicationDisplayName: "" };
4908
+ const applicationName = args2["applicationName"] ?? existing.applicationName;
4909
+ const applicationDisplayName = args2["applicationDisplayName"] ?? existing.applicationDisplayName ?? applicationName;
4910
+ await saveAppConfig(appKey, {
4911
+ ...existing,
4912
+ applicationName,
4913
+ applicationDisplayName
4914
+ });
4915
+ console.log(`App config for "${appKey}" updated.`);
4916
+ }
4917
+ async function standaloneConfigDeleteCliController(args2) {
4918
+ const appKey = args2["app"];
4919
+ if (!appKey) {
4920
+ console.error("Error: --app=platformName/applicationName is required.");
4921
+ process.exit(1);
4922
+ }
4923
+ const deleted = await deleteAppConfig(appKey);
4924
+ if (deleted) {
4925
+ console.log(`Deleted config for "${appKey}".`);
4926
+ } else {
4927
+ console.error(`Config for "${appKey}" not found.`);
4928
+ process.exit(1);
4929
+ }
4930
+ }
4931
+ async function standaloneConfigListCliController(_args) {
4932
+ const defaults = await getDefaults();
4933
+ const apps = await listAppConfigs();
4934
+ console.log(`Config file: ${getCachePath()}
4935
+ `);
4936
+ console.log("Defaults:");
4937
+ if (Object.keys(defaults).length === 0) {
4938
+ console.log(" (none)");
4939
+ } else {
4940
+ console.log(` Platform: ${defaults.platformName ?? "(not set)"}`);
4941
+ console.log(` Organization: ${defaults.organizationName ?? "(not set)"}`);
4942
+ console.log(` IDP: ${defaults.idp ? `${defaults.idp.provider} (${defaults.idp.name})` : "(not set)"}`);
4943
+ console.log(` Admin Users: ${defaults.adminUsers?.join(", ") ?? "(not set)"}`);
4944
+ }
4945
+ console.log("\nApps:");
4946
+ if (apps.length === 0) {
4947
+ console.log(" (none)");
4948
+ } else {
4949
+ for (const { key, resolved } of apps) {
4950
+ console.log(` ${key} \u2014 ${resolved.applicationDisplayName} [IDP: ${resolved.idp ? "yes" : "no"}, Admins: ${resolved.adminUsers?.length ?? 0}]`);
4951
+ }
4952
+ }
4953
+ }
4954
+
3728
4955
  // src/controllers/cli/registry.ts
3729
4956
  var cliControllers = /* @__PURE__ */ new Map([
3730
4957
  [CREATE_APPLICATION_COMMAND_NAME, createApplicationCliController],
@@ -3739,7 +4966,13 @@ var cliControllers = /* @__PURE__ */ new Map([
3739
4966
  [STOP_COMMAND_NAME, createLocalScriptCliController(STOP_COMMAND_NAME)],
3740
4967
  [DESTROY_COMMAND_NAME, createLocalScriptCliController(DESTROY_COMMAND_NAME)],
3741
4968
  [MANAGE_PLATFORM_ADMINS_COMMAND_NAME, managePlatformAdminsCliController],
3742
- [INSTALL_AI_PLUGIN_COMMAND_NAME, installAiPluginCliController]
4969
+ [INSTALL_AI_PLUGIN_COMMAND_NAME, installAiPluginCliController],
4970
+ [STANDALONE_COMMAND_NAME, standaloneCliController],
4971
+ [STANDALONE_STOP_COMMAND_NAME, standaloneStopCliController],
4972
+ [STANDALONE_CONFIG_SHOW_COMMAND_NAME, standaloneConfigShowCliController],
4973
+ [STANDALONE_CONFIG_SET_COMMAND_NAME, standaloneConfigSetCliController],
4974
+ [STANDALONE_CONFIG_DELETE_COMMAND_NAME, standaloneConfigDeleteCliController],
4975
+ [STANDALONE_CONFIG_LIST_COMMAND_NAME, standaloneConfigListCliController]
3743
4976
  ]);
3744
4977
 
3745
4978
  // src/utils/parse-args.ts