@bluealba/platform-cli 1.2.0-alpha.2 → 1.2.0-alpha.4
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 +716 -197
- package/package.json +1 -1
- package/skills/ba-platform/platform-add-feature-flag.skill.md +1 -1
- package/skills/ba-platform/platform-add-menu-item.skill.md +1 -1
- package/skills/ba-platform/platform-add-operation.skill.md +1 -1
- package/skills/ba-platform/platform-add-presence.skill.md +1 -1
- package/skills/ba-platform/platform-add-scheduled-job.skill.md +1 -1
- package/skills/ba-platform/platform-cli.skill.md +1 -1
- package/skills/ba-platform/platform-extend-shell.skill.md +1 -1
- package/skills/ba-platform/platform-scaffold-module.skill.md +1 -1
- package/skills/ba-platform/platform.skill.md +63 -20
- package/templates/application-monorepo-template/CLAUDE.md +36 -0
- package/templates/azure-pipeline-template/.azuredevops/azure-pipeline.yml +52 -0
- package/templates/github-pipeline-template/.github/workflows/build-platform.yml +24 -0
- package/templates/platform-init-template/{{platformName}}-core/CLAUDE.md +34 -0
package/dist/index.js
CHANGED
|
@@ -204,7 +204,7 @@ 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
|
|
207
|
+
import { join as join13, resolve as resolve2 } from "path";
|
|
208
208
|
import { cwd as cwd4 } from "process";
|
|
209
209
|
import { mkdir as mkdir3 } from "fs/promises";
|
|
210
210
|
|
|
@@ -730,6 +730,15 @@ async function getDefaults() {
|
|
|
730
730
|
const cache = await readCacheFile();
|
|
731
731
|
return cache.defaults;
|
|
732
732
|
}
|
|
733
|
+
var SENSITIVE_KEYS = /* @__PURE__ */ new Set(["clientSecret"]);
|
|
734
|
+
function maskSensitiveFields(obj) {
|
|
735
|
+
return JSON.parse(
|
|
736
|
+
JSON.stringify(
|
|
737
|
+
obj,
|
|
738
|
+
(key, value) => SENSITIVE_KEYS.has(key) && typeof value === "string" && value.length > 0 ? "***" : value
|
|
739
|
+
)
|
|
740
|
+
);
|
|
741
|
+
}
|
|
733
742
|
var LEGACY_CONFIG_FILE = "standalone.json";
|
|
734
743
|
async function readLegacyConfig(appDir) {
|
|
735
744
|
try {
|
|
@@ -742,6 +751,50 @@ async function readLegacyConfig(appDir) {
|
|
|
742
751
|
}
|
|
743
752
|
}
|
|
744
753
|
|
|
754
|
+
// src/utils/workspace-claude-md.ts
|
|
755
|
+
import { join as join12 } from "path";
|
|
756
|
+
import { writeFile as writeFile6 } from "fs/promises";
|
|
757
|
+
async function writeWorkspaceClaudeMd(rootDir, manifest) {
|
|
758
|
+
const { product, applications } = manifest;
|
|
759
|
+
const coreDirName = `${product.name}-core`;
|
|
760
|
+
const appLines = applications.map(
|
|
761
|
+
(app) => `- \`${product.name}-${app.name}/\` \u2014 ${app.displayName}: ${app.description}`
|
|
762
|
+
);
|
|
763
|
+
const appSection = appLines.length > 0 ? `
|
|
764
|
+
### Applications
|
|
765
|
+
|
|
766
|
+
${appLines.join("\n")}
|
|
767
|
+
` : "";
|
|
768
|
+
const content = `# ${product.displayName} Platform
|
|
769
|
+
|
|
770
|
+
This is a **Blue Alba Platform** workspace for \`@${product.organization}\`.
|
|
771
|
+
|
|
772
|
+
For any platform-related task, use the \`/platform\` skill \u2014 it routes to the correct specialized skill automatically.
|
|
773
|
+
|
|
774
|
+
## Workspace Layout
|
|
775
|
+
|
|
776
|
+
### Core
|
|
777
|
+
|
|
778
|
+
- \`${coreDirName}/\` \u2014 Core monorepo (bootstrap service, customization UI, local dev environment)
|
|
779
|
+
- \`${coreDirName}/local/\` \u2014 Docker Compose files, .env, SSL certificates
|
|
780
|
+
- \`${coreDirName}/local/platform-docker-compose.yml\` \u2014 Platform infrastructure (nginx, postgres, gateway)
|
|
781
|
+
- \`${coreDirName}/local/${product.name}-core-docker-compose.yml\` \u2014 Core application services
|
|
782
|
+
- \`${coreDirName}/local/.env\` \u2014 Environment variables (shared by all applications)
|
|
783
|
+
${appSection}
|
|
784
|
+
## Common Commands
|
|
785
|
+
|
|
786
|
+
\`\`\`sh
|
|
787
|
+
platform install # Install all monorepos
|
|
788
|
+
platform start # Start the full platform locally via Docker
|
|
789
|
+
platform stop # Stop all running containers
|
|
790
|
+
platform status # Show platform health and service status
|
|
791
|
+
platform create-application # Add a new application to this platform
|
|
792
|
+
platform standalone # Run a single application in isolation
|
|
793
|
+
\`\`\`
|
|
794
|
+
`;
|
|
795
|
+
await writeFile6(join12(rootDir, "CLAUDE.md"), content, "utf-8");
|
|
796
|
+
}
|
|
797
|
+
|
|
745
798
|
// src/commands/create-application/create-application.command.ts
|
|
746
799
|
var CREATE_APPLICATION_COMMAND_NAME = "create-application";
|
|
747
800
|
var createApplicationCommand = {
|
|
@@ -779,7 +832,7 @@ async function createApplication(params, logger) {
|
|
|
779
832
|
platformName = manifest.product.name;
|
|
780
833
|
organizationName = manifest.product.organization;
|
|
781
834
|
const localPath = `../${platformName}-${applicationName}`;
|
|
782
|
-
applicationDir = resolve2(
|
|
835
|
+
applicationDir = resolve2(join13(rootDir, coreDirName), localPath);
|
|
783
836
|
} else {
|
|
784
837
|
if (!params.platformName || !params.organizationName) {
|
|
785
838
|
logger.log("Error: platformName and organizationName are required when creating an application outside a platform.");
|
|
@@ -787,10 +840,10 @@ async function createApplication(params, logger) {
|
|
|
787
840
|
}
|
|
788
841
|
platformName = params.platformName;
|
|
789
842
|
organizationName = params.organizationName;
|
|
790
|
-
applicationDir =
|
|
843
|
+
applicationDir = join13(cwd4(), `${platformName}-${applicationName}`);
|
|
791
844
|
}
|
|
792
845
|
const bootstrapServiceName = `${platformName}-${applicationName}-bootstrap-service`;
|
|
793
|
-
const bootstrapServiceDir =
|
|
846
|
+
const bootstrapServiceDir = join13(applicationDir, "services", bootstrapServiceName);
|
|
794
847
|
logger.log(`Creating application monorepo "${applicationName}"...`);
|
|
795
848
|
try {
|
|
796
849
|
await scaffoldApplicationMonorepo(applicationDir, organizationName, platformName, applicationName, logger);
|
|
@@ -798,8 +851,8 @@ async function createApplication(params, logger) {
|
|
|
798
851
|
logger.log(`Error: Could not scaffold application monorepo \u2014 ${formatError(err)}`);
|
|
799
852
|
return;
|
|
800
853
|
}
|
|
801
|
-
await mkdir3(
|
|
802
|
-
await mkdir3(
|
|
854
|
+
await mkdir3(join13(applicationDir, "services"), { recursive: true });
|
|
855
|
+
await mkdir3(join13(applicationDir, "ui"), { recursive: true });
|
|
803
856
|
logger.log(`Creating bootstrap service "${bootstrapServiceName}"...`);
|
|
804
857
|
const bootstrapServiceBaseDir = `${platformName}-${applicationName}/services`;
|
|
805
858
|
try {
|
|
@@ -833,7 +886,7 @@ async function createApplication(params, logger) {
|
|
|
833
886
|
}
|
|
834
887
|
if (hasUserInterface) {
|
|
835
888
|
const uiName = `${platformName}-${applicationName}-ui`;
|
|
836
|
-
const uiDir =
|
|
889
|
+
const uiDir = join13(applicationDir, "ui", uiName);
|
|
837
890
|
const uiBaseDir = `${platformName}-${applicationName}/ui`;
|
|
838
891
|
try {
|
|
839
892
|
await scaffoldUiModule(uiDir, organizationName, platformName, applicationName, applicationDisplayName, uiBaseDir, logger);
|
|
@@ -844,7 +897,7 @@ async function createApplication(params, logger) {
|
|
|
844
897
|
}
|
|
845
898
|
if (hasBackendService) {
|
|
846
899
|
const serviceName = `${platformName}-${applicationName}-service`;
|
|
847
|
-
const serviceDir =
|
|
900
|
+
const serviceDir = join13(applicationDir, "services", serviceName);
|
|
848
901
|
const serviceBaseDir = `${platformName}-${applicationName}/services`;
|
|
849
902
|
try {
|
|
850
903
|
await scaffoldNestjsService(serviceDir, organizationName, platformName, applicationName, applicationDisplayName, serviceBaseDir, logger);
|
|
@@ -853,7 +906,7 @@ async function createApplication(params, logger) {
|
|
|
853
906
|
return;
|
|
854
907
|
}
|
|
855
908
|
}
|
|
856
|
-
const dockerComposePath =
|
|
909
|
+
const dockerComposePath = join13(applicationDir, "docker-compose.yml");
|
|
857
910
|
let uiPort;
|
|
858
911
|
let servicePort;
|
|
859
912
|
if (insidePlatform && localDir && manifest) {
|
|
@@ -889,6 +942,12 @@ async function createApplication(params, logger) {
|
|
|
889
942
|
} catch (err) {
|
|
890
943
|
logger.log(`Warning: Could not update product manifest \u2014 ${formatError(err)}`);
|
|
891
944
|
}
|
|
945
|
+
try {
|
|
946
|
+
await writeWorkspaceClaudeMd(rootDir, updatedManifest);
|
|
947
|
+
logger.log(`Updated workspace CLAUDE.md.`);
|
|
948
|
+
} catch (err) {
|
|
949
|
+
logger.log(`Warning: Could not update workspace CLAUDE.md \u2014 ${formatError(err)}`);
|
|
950
|
+
}
|
|
892
951
|
} else {
|
|
893
952
|
try {
|
|
894
953
|
await saveDefaults({ platformName, organizationName });
|
|
@@ -906,7 +965,7 @@ async function createApplication(params, logger) {
|
|
|
906
965
|
}
|
|
907
966
|
|
|
908
967
|
// src/commands/init/init.command.ts
|
|
909
|
-
import { join as
|
|
968
|
+
import { join as join19 } from "path";
|
|
910
969
|
import { cwd as cwd5 } from "process";
|
|
911
970
|
|
|
912
971
|
// src/utils/string.ts
|
|
@@ -916,8 +975,8 @@ function camelize(name) {
|
|
|
916
975
|
|
|
917
976
|
// src/commands/init/scaffold-platform.ts
|
|
918
977
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
919
|
-
import { join as
|
|
920
|
-
var templateDir =
|
|
978
|
+
import { join as join14, dirname as dirname8 } from "path";
|
|
979
|
+
var templateDir = join14(
|
|
921
980
|
dirname8(fileURLToPath6(import.meta.url)),
|
|
922
981
|
"..",
|
|
923
982
|
"templates",
|
|
@@ -929,8 +988,8 @@ async function scaffoldPlatform(outputDir, variables, logger) {
|
|
|
929
988
|
|
|
930
989
|
// src/commands/init/scaffold-platform-bootstrap.ts
|
|
931
990
|
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
932
|
-
import { join as
|
|
933
|
-
var templateDir2 =
|
|
991
|
+
import { join as join15, dirname as dirname9 } from "path";
|
|
992
|
+
var templateDir2 = join15(
|
|
934
993
|
dirname9(fileURLToPath7(import.meta.url)),
|
|
935
994
|
"..",
|
|
936
995
|
"templates",
|
|
@@ -942,8 +1001,8 @@ async function scaffoldPlatformBootstrap(outputDir, variables, logger) {
|
|
|
942
1001
|
|
|
943
1002
|
// src/commands/init/scaffold-customization-ui.ts
|
|
944
1003
|
import { fileURLToPath as fileURLToPath8 } from "url";
|
|
945
|
-
import { join as
|
|
946
|
-
var templateDir3 =
|
|
1004
|
+
import { join as join16, dirname as dirname10 } from "path";
|
|
1005
|
+
var templateDir3 = join16(
|
|
947
1006
|
dirname10(fileURLToPath8(import.meta.url)),
|
|
948
1007
|
"..",
|
|
949
1008
|
"templates",
|
|
@@ -954,10 +1013,10 @@ async function scaffoldCustomizationUi(outputDir, variables, logger) {
|
|
|
954
1013
|
}
|
|
955
1014
|
|
|
956
1015
|
// src/commands/init/register-customization-module.ts
|
|
957
|
-
import { join as
|
|
958
|
-
import { readFile as readFile6, writeFile as
|
|
1016
|
+
import { join as join17 } from "path";
|
|
1017
|
+
import { readFile as readFile6, writeFile as writeFile7 } from "fs/promises";
|
|
959
1018
|
async function registerCustomizationModule(bootstrapServiceDir, organizationName, logger, platformName = "platform") {
|
|
960
|
-
const modulesJsonPath =
|
|
1019
|
+
const modulesJsonPath = join17(bootstrapServiceDir, "src", "data", "platform", "modules.json");
|
|
961
1020
|
const customizationUiName = `${platformName}-customization-ui`;
|
|
962
1021
|
const entry = {
|
|
963
1022
|
name: `@${organizationName}/${customizationUiName}`,
|
|
@@ -975,13 +1034,13 @@ async function registerCustomizationModule(bootstrapServiceDir, organizationName
|
|
|
975
1034
|
};
|
|
976
1035
|
const existing = JSON.parse(await readFile6(modulesJsonPath, "utf-8"));
|
|
977
1036
|
existing.push(entry);
|
|
978
|
-
await
|
|
1037
|
+
await writeFile7(modulesJsonPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
979
1038
|
logger.log(`Registered customization module in ${modulesJsonPath}`);
|
|
980
1039
|
}
|
|
981
1040
|
|
|
982
1041
|
// src/commands/init/generate-local-env.ts
|
|
983
|
-
import { readFile as readFile7, writeFile as
|
|
984
|
-
import { join as
|
|
1042
|
+
import { readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
|
|
1043
|
+
import { join as join18 } from "path";
|
|
985
1044
|
|
|
986
1045
|
// src/utils/random.ts
|
|
987
1046
|
import { randomBytes } from "crypto";
|
|
@@ -991,14 +1050,14 @@ function generateRandomSecret() {
|
|
|
991
1050
|
|
|
992
1051
|
// src/commands/init/generate-local-env.ts
|
|
993
1052
|
async function generateLocalEnv(outputDir, logger, coreDirName = "core") {
|
|
994
|
-
const examplePath =
|
|
995
|
-
const envPath =
|
|
1053
|
+
const examplePath = join18(outputDir, coreDirName, "local", ".env.example");
|
|
1054
|
+
const envPath = join18(outputDir, coreDirName, "local", ".env");
|
|
996
1055
|
logger.log(`Generating ${coreDirName}/local/.env with random secrets...`);
|
|
997
1056
|
const content = await readFile7(examplePath, "utf-8");
|
|
998
1057
|
const result = content.replace(/^(PAE_AUTH_JWT_SECRET|PAE_GATEWAY_SERVICE_ACCESS_SECRET|PAE_DB_PASSWORD)=$/gm, (_, key) => {
|
|
999
1058
|
return `${key}=${generateRandomSecret()}`;
|
|
1000
1059
|
});
|
|
1001
|
-
await
|
|
1060
|
+
await writeFile8(envPath, result, "utf-8");
|
|
1002
1061
|
}
|
|
1003
1062
|
|
|
1004
1063
|
// src/commands/init/init.command.ts
|
|
@@ -1044,17 +1103,24 @@ async function init(params, logger) {
|
|
|
1044
1103
|
logger.log(`Error: Could not generate ${coreDirName}/local/.env \u2014 ${formatError(err)}`);
|
|
1045
1104
|
return;
|
|
1046
1105
|
}
|
|
1106
|
+
let manifest;
|
|
1047
1107
|
try {
|
|
1048
|
-
|
|
1108
|
+
manifest = createInitialManifest({ organizationName, platformName, platformDisplayName });
|
|
1049
1109
|
await writeManifest(manifest, outputDir, coreDirName);
|
|
1050
1110
|
logger.log(`Created product manifest: ${coreDirName}/product.manifest.json`);
|
|
1051
1111
|
} catch (err) {
|
|
1052
1112
|
logger.log(`Error: Could not write product manifest \u2014 ${formatError(err)}`);
|
|
1053
1113
|
return;
|
|
1054
1114
|
}
|
|
1115
|
+
try {
|
|
1116
|
+
await writeWorkspaceClaudeMd(outputDir, manifest);
|
|
1117
|
+
logger.log(`Created workspace CLAUDE.md`);
|
|
1118
|
+
} catch (err) {
|
|
1119
|
+
logger.log(`Warning: Could not write workspace CLAUDE.md \u2014 ${formatError(err)}`);
|
|
1120
|
+
}
|
|
1055
1121
|
try {
|
|
1056
1122
|
await scaffoldPlatformBootstrap(
|
|
1057
|
-
|
|
1123
|
+
join19(outputDir, coreDirName, "services", bootstrapServiceName),
|
|
1058
1124
|
variables,
|
|
1059
1125
|
logger
|
|
1060
1126
|
);
|
|
@@ -1064,7 +1130,7 @@ async function init(params, logger) {
|
|
|
1064
1130
|
}
|
|
1065
1131
|
try {
|
|
1066
1132
|
await scaffoldCustomizationUi(
|
|
1067
|
-
|
|
1133
|
+
join19(outputDir, coreDirName, "ui", customizationUiName),
|
|
1068
1134
|
variables,
|
|
1069
1135
|
logger
|
|
1070
1136
|
);
|
|
@@ -1074,7 +1140,7 @@ async function init(params, logger) {
|
|
|
1074
1140
|
}
|
|
1075
1141
|
try {
|
|
1076
1142
|
await registerCustomizationModule(
|
|
1077
|
-
|
|
1143
|
+
join19(outputDir, coreDirName, "services", bootstrapServiceName),
|
|
1078
1144
|
organizationName,
|
|
1079
1145
|
logger,
|
|
1080
1146
|
platformName
|
|
@@ -1087,7 +1153,7 @@ async function init(params, logger) {
|
|
|
1087
1153
|
}
|
|
1088
1154
|
|
|
1089
1155
|
// src/commands/configure-idp/configure-idp.command.ts
|
|
1090
|
-
import { join as
|
|
1156
|
+
import { join as join20 } from "path";
|
|
1091
1157
|
import { fetch as undiciFetch, Agent } from "undici";
|
|
1092
1158
|
|
|
1093
1159
|
// src/utils/env-reader.ts
|
|
@@ -1171,7 +1237,7 @@ async function configureIdp(params, logger) {
|
|
|
1171
1237
|
logger.log("Error: Cannot configure an IDP \u2014 no platform initialized in this directory.");
|
|
1172
1238
|
return;
|
|
1173
1239
|
}
|
|
1174
|
-
const envPath =
|
|
1240
|
+
const envPath = join20(layout.localDir, ".env");
|
|
1175
1241
|
let env;
|
|
1176
1242
|
try {
|
|
1177
1243
|
env = await readEnvFile(envPath);
|
|
@@ -1223,13 +1289,13 @@ async function configureIdp(params, logger) {
|
|
|
1223
1289
|
}
|
|
1224
1290
|
|
|
1225
1291
|
// src/commands/create-service-module/create-service-module.command.ts
|
|
1226
|
-
import { join as
|
|
1227
|
-
import { access as
|
|
1292
|
+
import { join as join22, resolve as resolve3 } from "path";
|
|
1293
|
+
import { access as access5 } from "fs/promises";
|
|
1228
1294
|
|
|
1229
1295
|
// src/commands/create-service-module/scaffold-service-module.ts
|
|
1230
1296
|
import { fileURLToPath as fileURLToPath9 } from "url";
|
|
1231
|
-
import { join as
|
|
1232
|
-
var nestjsServiceModuleTemplateDir2 =
|
|
1297
|
+
import { join as join21, dirname as dirname11 } from "path";
|
|
1298
|
+
var nestjsServiceModuleTemplateDir2 = join21(
|
|
1233
1299
|
dirname11(fileURLToPath9(import.meta.url)),
|
|
1234
1300
|
"..",
|
|
1235
1301
|
"templates",
|
|
@@ -1264,7 +1330,7 @@ function buildCustomServiceModuleEntry(organizationName, serviceName, serviceDis
|
|
|
1264
1330
|
}
|
|
1265
1331
|
|
|
1266
1332
|
// src/commands/create-service-module/append-docker-compose.ts
|
|
1267
|
-
import { readFile as readFile9, writeFile as
|
|
1333
|
+
import { readFile as readFile9, writeFile as writeFile9 } from "fs/promises";
|
|
1268
1334
|
async function appendServiceToDockerCompose(dockerComposePath, serviceName, platformName, applicationName, port, logger) {
|
|
1269
1335
|
const block = `
|
|
1270
1336
|
${serviceName}:
|
|
@@ -1280,7 +1346,7 @@ async function appendServiceToDockerCompose(dockerComposePath, serviceName, plat
|
|
|
1280
1346
|
`;
|
|
1281
1347
|
try {
|
|
1282
1348
|
const existing = await readFile9(dockerComposePath, "utf-8");
|
|
1283
|
-
await
|
|
1349
|
+
await writeFile9(dockerComposePath, existing + block, "utf-8");
|
|
1284
1350
|
logger.log(`Updated docker-compose: ${dockerComposePath}`);
|
|
1285
1351
|
} catch (err) {
|
|
1286
1352
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1288,6 +1354,46 @@ async function appendServiceToDockerCompose(dockerComposePath, serviceName, plat
|
|
|
1288
1354
|
}
|
|
1289
1355
|
}
|
|
1290
1356
|
|
|
1357
|
+
// src/commands/pipelines/update-azure-pipeline-services.ts
|
|
1358
|
+
import { readFile as readFile10, writeFile as writeFile10, access as access4 } from "fs/promises";
|
|
1359
|
+
var END_MARKER = "# platform-cli:managed-services-end";
|
|
1360
|
+
async function addServiceToAzurePipeline(pipelinePath, serviceName, serviceFolder, logger) {
|
|
1361
|
+
try {
|
|
1362
|
+
await access4(pipelinePath);
|
|
1363
|
+
} catch {
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
let content;
|
|
1367
|
+
try {
|
|
1368
|
+
content = await readFile10(pipelinePath, "utf-8");
|
|
1369
|
+
} catch (err) {
|
|
1370
|
+
logger.log(`Warning: Could not read Azure pipeline file \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
if (!content.includes(END_MARKER)) {
|
|
1374
|
+
logger.log(`Warning: Azure pipeline file does not contain managed services markers \u2014 skipping automatic update.`);
|
|
1375
|
+
logger.log(`Add the service manually to .azuredevops/azure-pipeline.yml`);
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
if (content.includes(`- name: ${serviceName}`)) {
|
|
1379
|
+
logger.log(`Service "${serviceName}" is already listed in the Azure pipeline \u2014 skipping.`);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
const newEntry = ` - name: ${serviceName}
|
|
1383
|
+
folder: ${serviceFolder}
|
|
1384
|
+
`;
|
|
1385
|
+
const updated = content.replace(
|
|
1386
|
+
` ${END_MARKER}`,
|
|
1387
|
+
`${newEntry} ${END_MARKER}`
|
|
1388
|
+
);
|
|
1389
|
+
try {
|
|
1390
|
+
await writeFile10(pipelinePath, updated, "utf-8");
|
|
1391
|
+
logger.log(`Updated Azure pipeline: added "${serviceName}" to services list.`);
|
|
1392
|
+
} catch (err) {
|
|
1393
|
+
logger.log(`Warning: Could not update Azure pipeline \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1291
1397
|
// src/commands/create-service-module/create-service-module.command.ts
|
|
1292
1398
|
var CREATE_SERVICE_MODULE_COMMAND_NAME = "create-service-module";
|
|
1293
1399
|
var createServiceModuleCommand = {
|
|
@@ -1316,18 +1422,18 @@ async function createServiceModule(params, logger) {
|
|
|
1316
1422
|
logger.log(`Error: The specified application "${applicationName}" is not registered in the product manifest.`);
|
|
1317
1423
|
return;
|
|
1318
1424
|
}
|
|
1319
|
-
const applicationDir = resolve3(
|
|
1425
|
+
const applicationDir = resolve3(join22(rootDir, coreDirName), appEntry.localPath);
|
|
1320
1426
|
try {
|
|
1321
|
-
await
|
|
1427
|
+
await access5(applicationDir);
|
|
1322
1428
|
} catch {
|
|
1323
1429
|
logger.log(`Error: The specified application "${applicationName}" does not exist in the platform.`);
|
|
1324
1430
|
return;
|
|
1325
1431
|
}
|
|
1326
1432
|
const suffix = serviceNameSuffix === void 0 ? "service" : serviceNameSuffix;
|
|
1327
1433
|
const fullServiceName = suffix ? `${platformName}-${serviceName}-${suffix}` : `${platformName}-${serviceName}`;
|
|
1328
|
-
const serviceDir =
|
|
1434
|
+
const serviceDir = join22(applicationDir, "services", fullServiceName);
|
|
1329
1435
|
try {
|
|
1330
|
-
await
|
|
1436
|
+
await access5(serviceDir);
|
|
1331
1437
|
logger.log(`Error: A service named "${fullServiceName}" already exists in application "${applicationName}".`);
|
|
1332
1438
|
return;
|
|
1333
1439
|
} catch {
|
|
@@ -1346,21 +1452,23 @@ async function createServiceModule(params, logger) {
|
|
|
1346
1452
|
logger.log(`Error: Could not scaffold NestJS service \u2014 ${formatError(err)}`);
|
|
1347
1453
|
return;
|
|
1348
1454
|
}
|
|
1349
|
-
const bootstrapServiceDir =
|
|
1455
|
+
const bootstrapServiceDir = join22(applicationDir, "services", `${platformName}-${applicationName}-bootstrap-service`);
|
|
1350
1456
|
const moduleEntry = buildCustomServiceModuleEntry(organizationName, fullServiceName, serviceDisplayName);
|
|
1351
1457
|
await addModuleEntry(bootstrapServiceDir, applicationName, moduleEntry, logger);
|
|
1352
|
-
const dockerComposePath =
|
|
1458
|
+
const dockerComposePath = join22(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
|
|
1353
1459
|
const port = await getNextAvailablePort(localDir);
|
|
1354
1460
|
await appendServiceToDockerCompose(dockerComposePath, fullServiceName, platformName, applicationName, port, logger);
|
|
1461
|
+
const azurePipelinePath = join22(applicationDir, ".azuredevops", "azure-pipeline.yml");
|
|
1462
|
+
await addServiceToAzurePipeline(azurePipelinePath, fullServiceName, `services/${fullServiceName}`, logger);
|
|
1355
1463
|
logger.log(`Done! Service module "${fullServiceName}" added to application "${applicationName}".`);
|
|
1356
1464
|
}
|
|
1357
1465
|
|
|
1358
1466
|
// src/commands/create-ui-module/create-ui-module.command.ts
|
|
1359
|
-
import { join as
|
|
1360
|
-
import { access as
|
|
1467
|
+
import { join as join23, resolve as resolve4 } from "path";
|
|
1468
|
+
import { access as access6, readdir as readdir4 } from "fs/promises";
|
|
1361
1469
|
|
|
1362
1470
|
// src/commands/create-ui-module/append-ui-docker-compose.ts
|
|
1363
|
-
import { readFile as
|
|
1471
|
+
import { readFile as readFile11, writeFile as writeFile11 } from "fs/promises";
|
|
1364
1472
|
async function appendUiToDockerCompose(dockerComposePath, platformName, applicationName, port, logger, uiNameOverride) {
|
|
1365
1473
|
const uiName = uiNameOverride ?? `${platformName}-${applicationName}-ui`;
|
|
1366
1474
|
const block = `
|
|
@@ -1374,8 +1482,8 @@ async function appendUiToDockerCompose(dockerComposePath, platformName, applicat
|
|
|
1374
1482
|
- \${PWD}/:/app/out
|
|
1375
1483
|
`;
|
|
1376
1484
|
try {
|
|
1377
|
-
const existing = await
|
|
1378
|
-
await
|
|
1485
|
+
const existing = await readFile11(dockerComposePath, "utf-8");
|
|
1486
|
+
await writeFile11(dockerComposePath, existing + block, "utf-8");
|
|
1379
1487
|
logger.log(`Updated docker-compose: ${dockerComposePath}`);
|
|
1380
1488
|
} catch (err) {
|
|
1381
1489
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1411,14 +1519,14 @@ async function createUiModule(params, logger) {
|
|
|
1411
1519
|
logger.log(`Error: The specified application "${applicationName}" is not registered in the product manifest.`);
|
|
1412
1520
|
return;
|
|
1413
1521
|
}
|
|
1414
|
-
const applicationDir = resolve4(
|
|
1522
|
+
const applicationDir = resolve4(join23(rootDir, coreDirName), appEntry.localPath);
|
|
1415
1523
|
try {
|
|
1416
|
-
await
|
|
1524
|
+
await access6(applicationDir);
|
|
1417
1525
|
} catch {
|
|
1418
1526
|
logger.log(`Error: The specified application "${applicationName}" does not exist in the platform.`);
|
|
1419
1527
|
return;
|
|
1420
1528
|
}
|
|
1421
|
-
const uiDir =
|
|
1529
|
+
const uiDir = join23(applicationDir, "ui");
|
|
1422
1530
|
try {
|
|
1423
1531
|
const uiEntries = await readdir4(uiDir);
|
|
1424
1532
|
const existingUiModules = uiEntries.filter((e) => e !== ".gitkeep");
|
|
@@ -1430,7 +1538,7 @@ async function createUiModule(params, logger) {
|
|
|
1430
1538
|
}
|
|
1431
1539
|
const uiSuffix = uiModuleSuffix === void 0 ? "ui" : uiModuleSuffix;
|
|
1432
1540
|
const uiName = uiSuffix ? `${platformName}-${applicationName}-${uiSuffix}` : `${platformName}-${applicationName}`;
|
|
1433
|
-
const uiOutputDir =
|
|
1541
|
+
const uiOutputDir = join23(uiDir, uiName);
|
|
1434
1542
|
const uiBaseDir = `${platformName}-${applicationName}/ui`;
|
|
1435
1543
|
try {
|
|
1436
1544
|
await scaffoldUiModule(uiOutputDir, organizationName, platformName, applicationName, applicationDisplayName, uiBaseDir, logger);
|
|
@@ -1438,25 +1546,27 @@ async function createUiModule(params, logger) {
|
|
|
1438
1546
|
logger.log(`Error: Could not scaffold UI module \u2014 ${formatError(err)}`);
|
|
1439
1547
|
return;
|
|
1440
1548
|
}
|
|
1441
|
-
const bootstrapServiceDir =
|
|
1549
|
+
const bootstrapServiceDir = join23(applicationDir, "services", `${platformName}-${applicationName}-bootstrap-service`);
|
|
1442
1550
|
const moduleEntry = buildUiModuleEntry(organizationName, platformName, applicationName, applicationDisplayName, uiName);
|
|
1443
1551
|
await addModuleEntry(bootstrapServiceDir, applicationName, moduleEntry, logger);
|
|
1444
|
-
const dockerComposePath =
|
|
1552
|
+
const dockerComposePath = join23(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
|
|
1445
1553
|
const port = await getNextAvailablePort(localDir);
|
|
1446
1554
|
await appendUiToDockerCompose(dockerComposePath, platformName, applicationName, port, logger, uiName);
|
|
1555
|
+
const azurePipelinePath = join23(applicationDir, ".azuredevops", "azure-pipeline.yml");
|
|
1556
|
+
await addServiceToAzurePipeline(azurePipelinePath, uiName, `ui/${uiName}`, logger);
|
|
1447
1557
|
logger.log(`Done! UI module "${uiName}" added to application "${applicationName}".`);
|
|
1448
1558
|
}
|
|
1449
1559
|
|
|
1450
1560
|
// src/commands/status/status-checks.ts
|
|
1451
1561
|
import { spawn as spawn3 } from "child_process";
|
|
1452
|
-
import { access as
|
|
1453
|
-
import { join as
|
|
1562
|
+
import { access as access9 } from "fs/promises";
|
|
1563
|
+
import { join as join26, resolve as resolve7 } from "path";
|
|
1454
1564
|
import { fetch as undiciFetch2, Agent as Agent2 } from "undici";
|
|
1455
1565
|
|
|
1456
1566
|
// src/commands/local-scripts/docker-compose-orchestrator.ts
|
|
1457
1567
|
import { spawn } from "child_process";
|
|
1458
|
-
import { access as
|
|
1459
|
-
import { join as
|
|
1568
|
+
import { access as access7, readFile as readFile12, writeFile as writeFile12 } from "fs/promises";
|
|
1569
|
+
import { dirname as dirname12, join as join24, resolve as resolve5 } from "path";
|
|
1460
1570
|
function runDockerCompose(args2, logger, rootDir, signal) {
|
|
1461
1571
|
return new Promise((resolvePromise) => {
|
|
1462
1572
|
const child = spawn("docker", ["compose", ...args2], {
|
|
@@ -1519,25 +1629,38 @@ function captureDockerCompose(args2, rootDir) {
|
|
|
1519
1629
|
child.on("error", (err) => reject(err));
|
|
1520
1630
|
});
|
|
1521
1631
|
}
|
|
1632
|
+
async function generateProxyAppCompose(appDir, localDir, platformName, applicationName) {
|
|
1633
|
+
const appComposePath = join24(appDir, "docker-compose.yml");
|
|
1634
|
+
const content = await readFile12(appComposePath, "utf-8");
|
|
1635
|
+
const withAbsContexts = content.replace(
|
|
1636
|
+
/^(\s+context:\s+)\.\/(.*)$/gm,
|
|
1637
|
+
`$1${appDir}/$2`
|
|
1638
|
+
);
|
|
1639
|
+
const appParentDir = dirname12(appDir);
|
|
1640
|
+
const rewritten = withAbsContexts.replace(/\$\{PWD\}/g, appParentDir);
|
|
1641
|
+
const proxyPath = join24(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
|
|
1642
|
+
await writeFile12(proxyPath, rewritten, "utf-8");
|
|
1643
|
+
return proxyPath;
|
|
1644
|
+
}
|
|
1522
1645
|
async function getAppComposePaths(localDir, coreDir, platformName, manifest, logger) {
|
|
1523
1646
|
const results = [];
|
|
1524
1647
|
for (const app of manifest.applications) {
|
|
1525
|
-
const appDir = resolve5(
|
|
1526
|
-
const newPath =
|
|
1527
|
-
const legacyPrefixedPath =
|
|
1528
|
-
const legacyUnprefixedPath =
|
|
1648
|
+
const appDir = resolve5(join24(coreDir), app.localPath);
|
|
1649
|
+
const newPath = join24(appDir, "docker-compose.yml");
|
|
1650
|
+
const legacyPrefixedPath = join24(localDir, `${platformName}-${app.name}-docker-compose.yml`);
|
|
1651
|
+
const legacyUnprefixedPath = join24(localDir, `${app.name}-docker-compose.yml`);
|
|
1529
1652
|
let resolved = null;
|
|
1530
1653
|
try {
|
|
1531
|
-
await
|
|
1532
|
-
resolved =
|
|
1654
|
+
await access7(newPath);
|
|
1655
|
+
resolved = await generateProxyAppCompose(appDir, localDir, platformName, app.name);
|
|
1533
1656
|
} catch {
|
|
1534
1657
|
try {
|
|
1535
|
-
await
|
|
1658
|
+
await access7(legacyPrefixedPath);
|
|
1536
1659
|
resolved = legacyPrefixedPath;
|
|
1537
1660
|
logger?.log(`Warning: docker-compose for "${app.name}" found in legacy location (core/local/). Consider moving it to the app monorepo root.`);
|
|
1538
1661
|
} catch {
|
|
1539
1662
|
try {
|
|
1540
|
-
await
|
|
1663
|
+
await access7(legacyUnprefixedPath);
|
|
1541
1664
|
resolved = legacyUnprefixedPath;
|
|
1542
1665
|
logger?.log(`Warning: docker-compose for "${app.name}" found in legacy location (core/local/). Consider moving it to the app monorepo root.`);
|
|
1543
1666
|
} catch {
|
|
@@ -1553,18 +1676,18 @@ async function getAppComposePaths(localDir, coreDir, platformName, manifest, log
|
|
|
1553
1676
|
async function buildFullComposeArgs(layout, manifest, logger) {
|
|
1554
1677
|
const { coreDirName, localDir, coreDir } = layout;
|
|
1555
1678
|
const platformName = manifest.product.name;
|
|
1556
|
-
const prefixedCoreCompose =
|
|
1557
|
-
const unprefixedCoreCompose =
|
|
1679
|
+
const prefixedCoreCompose = join24(localDir, `${coreDirName}-docker-compose.yml`);
|
|
1680
|
+
const unprefixedCoreCompose = join24(localDir, "core-docker-compose.yml");
|
|
1558
1681
|
let coreComposePath;
|
|
1559
1682
|
try {
|
|
1560
|
-
await
|
|
1683
|
+
await access7(prefixedCoreCompose);
|
|
1561
1684
|
coreComposePath = prefixedCoreCompose;
|
|
1562
1685
|
} catch {
|
|
1563
1686
|
coreComposePath = unprefixedCoreCompose;
|
|
1564
1687
|
}
|
|
1565
1688
|
const fileArgs = [
|
|
1566
1689
|
"-f",
|
|
1567
|
-
|
|
1690
|
+
join24(localDir, "platform-docker-compose.yml"),
|
|
1568
1691
|
"-f",
|
|
1569
1692
|
coreComposePath
|
|
1570
1693
|
];
|
|
@@ -1584,16 +1707,16 @@ async function buildSelectedComposeFiles(layout, selectedManifest, includeCore)
|
|
|
1584
1707
|
const platformName = selectedManifest.product.name;
|
|
1585
1708
|
const files = [];
|
|
1586
1709
|
if (includeCore) {
|
|
1587
|
-
const prefixedCoreCompose =
|
|
1588
|
-
const unprefixedCoreCompose =
|
|
1710
|
+
const prefixedCoreCompose = join24(localDir, `${coreDirName}-docker-compose.yml`);
|
|
1711
|
+
const unprefixedCoreCompose = join24(localDir, "core-docker-compose.yml");
|
|
1589
1712
|
let coreComposePath;
|
|
1590
1713
|
try {
|
|
1591
|
-
await
|
|
1714
|
+
await access7(prefixedCoreCompose);
|
|
1592
1715
|
coreComposePath = prefixedCoreCompose;
|
|
1593
1716
|
} catch {
|
|
1594
1717
|
coreComposePath = unprefixedCoreCompose;
|
|
1595
1718
|
}
|
|
1596
|
-
files.push(
|
|
1719
|
+
files.push(join24(localDir, "platform-docker-compose.yml"), coreComposePath);
|
|
1597
1720
|
}
|
|
1598
1721
|
const appEntries = await getAppComposePaths(localDir, coreDir, platformName, selectedManifest);
|
|
1599
1722
|
for (const { composePath } of appEntries) {
|
|
@@ -1637,7 +1760,7 @@ async function getServicesFromComposeFiles(selectedFiles, allFiles, rootDir) {
|
|
|
1637
1760
|
async function startEnvironment(layout, manifest, logger, signal, includeCore = true, fullManifest) {
|
|
1638
1761
|
const { rootDir, localDir } = layout;
|
|
1639
1762
|
const platformName = manifest.product.name;
|
|
1640
|
-
const envFile =
|
|
1763
|
+
const envFile = join24(localDir, ".env");
|
|
1641
1764
|
const isSelective = fullManifest !== void 0;
|
|
1642
1765
|
const fileArgs = await buildFullComposeArgs(layout, isSelective ? fullManifest : manifest, logger);
|
|
1643
1766
|
const projectArgs = ["-p", `${platformName}-platform`, "--project-directory", localDir, "--env-file", envFile];
|
|
@@ -1659,7 +1782,7 @@ async function startEnvironment(layout, manifest, logger, signal, includeCore =
|
|
|
1659
1782
|
async function stopEnvironment(layout, manifest, logger, signal, includeCore = true, fullManifest) {
|
|
1660
1783
|
const { rootDir, localDir } = layout;
|
|
1661
1784
|
const platformName = manifest.product.name;
|
|
1662
|
-
const envFile =
|
|
1785
|
+
const envFile = join24(localDir, ".env");
|
|
1663
1786
|
const isSelective = fullManifest !== void 0;
|
|
1664
1787
|
const fileArgs = await buildFullComposeArgs(layout, isSelective ? fullManifest : manifest, logger);
|
|
1665
1788
|
const projectArgs = ["-p", `${platformName}-platform`, "--project-directory", localDir, "--env-file", envFile];
|
|
@@ -1681,7 +1804,7 @@ async function stopEnvironment(layout, manifest, logger, signal, includeCore = t
|
|
|
1681
1804
|
async function resetEnvironment(layout, manifest, logger, signal) {
|
|
1682
1805
|
const { rootDir, localDir } = layout;
|
|
1683
1806
|
const platformName = manifest.product.name;
|
|
1684
|
-
const envFile =
|
|
1807
|
+
const envFile = join24(localDir, ".env");
|
|
1685
1808
|
const fileArgs = await buildFullComposeArgs(layout, manifest, logger);
|
|
1686
1809
|
const projectArgs = ["-p", `${platformName}-platform`, "--project-directory", localDir, "--env-file", envFile];
|
|
1687
1810
|
await runDockerCompose([...projectArgs, ...fileArgs, "down", "-v"], logger, rootDir, signal);
|
|
@@ -1689,7 +1812,7 @@ async function resetEnvironment(layout, manifest, logger, signal) {
|
|
|
1689
1812
|
async function destroyEnvironment(layout, manifest, logger, signal, includeCore = true, fullManifest, removeImages = true) {
|
|
1690
1813
|
const { rootDir, localDir } = layout;
|
|
1691
1814
|
const platformName = manifest.product.name;
|
|
1692
|
-
const envFile =
|
|
1815
|
+
const envFile = join24(localDir, ".env");
|
|
1693
1816
|
const isSelective = fullManifest !== void 0;
|
|
1694
1817
|
const fileArgs = await buildFullComposeArgs(layout, isSelective ? fullManifest : manifest, logger);
|
|
1695
1818
|
const projectArgs = ["-p", `${platformName}-platform`, "--project-directory", localDir, "--env-file", envFile];
|
|
@@ -1715,8 +1838,8 @@ async function destroyEnvironment(layout, manifest, logger, signal, includeCore
|
|
|
1715
1838
|
|
|
1716
1839
|
// src/commands/local-scripts/npm-orchestrator.ts
|
|
1717
1840
|
import { spawn as spawn2 } from "child_process";
|
|
1718
|
-
import { access as
|
|
1719
|
-
import { resolve as resolve6, join as
|
|
1841
|
+
import { access as access8 } from "fs/promises";
|
|
1842
|
+
import { resolve as resolve6, join as join25 } from "path";
|
|
1720
1843
|
function runCommand(command, args2, workDir, logger, signal) {
|
|
1721
1844
|
return new Promise((resolvePromise) => {
|
|
1722
1845
|
const child = spawn2(command, args2, {
|
|
@@ -1763,7 +1886,7 @@ function runCommand(command, args2, workDir, logger, signal) {
|
|
|
1763
1886
|
}
|
|
1764
1887
|
async function dirExists(dirPath) {
|
|
1765
1888
|
try {
|
|
1766
|
-
await
|
|
1889
|
+
await access8(dirPath);
|
|
1767
1890
|
return true;
|
|
1768
1891
|
} catch {
|
|
1769
1892
|
return false;
|
|
@@ -1773,7 +1896,7 @@ async function installDependencies(layout, manifest, logger, signal, includeCore
|
|
|
1773
1896
|
const { coreDir, coreDirName } = layout;
|
|
1774
1897
|
const appDirs = [];
|
|
1775
1898
|
for (const app of manifest.applications) {
|
|
1776
|
-
const appDir = resolve6(
|
|
1899
|
+
const appDir = resolve6(join25(coreDir), app.localPath);
|
|
1777
1900
|
if (!await dirExists(appDir)) {
|
|
1778
1901
|
logger.log(`Warning: Application directory "${app.name}" not found at ${appDir} \u2014 skipping install.`);
|
|
1779
1902
|
continue;
|
|
@@ -1799,7 +1922,7 @@ async function buildAll(layout, manifest, logger, signal, includeCore = true) {
|
|
|
1799
1922
|
}
|
|
1800
1923
|
const appDirs = [];
|
|
1801
1924
|
for (const app of manifest.applications) {
|
|
1802
|
-
const appDir = resolve6(
|
|
1925
|
+
const appDir = resolve6(join25(coreDir), app.localPath);
|
|
1803
1926
|
if (!await dirExists(appDir)) {
|
|
1804
1927
|
logger.log(`Warning: Application directory "${app.name}" not found at ${appDir} \u2014 skipping build.`);
|
|
1805
1928
|
continue;
|
|
@@ -1936,7 +2059,7 @@ function spawnCapture(cmd, args2, cwd11) {
|
|
|
1936
2059
|
}
|
|
1937
2060
|
async function pathExists(p) {
|
|
1938
2061
|
try {
|
|
1939
|
-
await
|
|
2062
|
+
await access9(p);
|
|
1940
2063
|
return true;
|
|
1941
2064
|
} catch {
|
|
1942
2065
|
return false;
|
|
@@ -1980,13 +2103,13 @@ async function checkInstalled(layout, manifest) {
|
|
|
1980
2103
|
const results = [];
|
|
1981
2104
|
const coreCheck = {
|
|
1982
2105
|
name: layout.coreDirName,
|
|
1983
|
-
path:
|
|
1984
|
-
exists: await pathExists(
|
|
2106
|
+
path: join26(layout.coreDir, "node_modules"),
|
|
2107
|
+
exists: await pathExists(join26(layout.coreDir, "node_modules"))
|
|
1985
2108
|
};
|
|
1986
2109
|
results.push(coreCheck);
|
|
1987
2110
|
for (const app of manifest.applications) {
|
|
1988
2111
|
const appDir = resolve7(layout.coreDir, app.localPath);
|
|
1989
|
-
const nodeModulesPath =
|
|
2112
|
+
const nodeModulesPath = join26(appDir, "node_modules");
|
|
1990
2113
|
results.push({
|
|
1991
2114
|
name: app.name,
|
|
1992
2115
|
path: nodeModulesPath,
|
|
@@ -1997,7 +2120,7 @@ async function checkInstalled(layout, manifest) {
|
|
|
1997
2120
|
}
|
|
1998
2121
|
async function checkBuilt(layout, manifest) {
|
|
1999
2122
|
const results = [];
|
|
2000
|
-
const coreTurboPath =
|
|
2123
|
+
const coreTurboPath = join26(layout.coreDir, ".turbo");
|
|
2001
2124
|
results.push({
|
|
2002
2125
|
name: layout.coreDirName,
|
|
2003
2126
|
path: coreTurboPath,
|
|
@@ -2005,7 +2128,7 @@ async function checkBuilt(layout, manifest) {
|
|
|
2005
2128
|
});
|
|
2006
2129
|
for (const app of manifest.applications) {
|
|
2007
2130
|
const appDir = resolve7(layout.coreDir, app.localPath);
|
|
2008
|
-
const appTurboPath =
|
|
2131
|
+
const appTurboPath = join26(appDir, ".turbo");
|
|
2009
2132
|
results.push({
|
|
2010
2133
|
name: app.name,
|
|
2011
2134
|
path: appTurboPath,
|
|
@@ -2017,13 +2140,13 @@ async function checkBuilt(layout, manifest) {
|
|
|
2017
2140
|
async function resolveComposeFiles(layout, manifest) {
|
|
2018
2141
|
const { localDir, coreDirName } = layout;
|
|
2019
2142
|
const platformName = manifest.product.name;
|
|
2020
|
-
const files = [
|
|
2021
|
-
const prefixedCore =
|
|
2022
|
-
const unprefixedCore =
|
|
2143
|
+
const files = [join26(localDir, "platform-docker-compose.yml")];
|
|
2144
|
+
const prefixedCore = join26(localDir, `${coreDirName}-docker-compose.yml`);
|
|
2145
|
+
const unprefixedCore = join26(localDir, "core-docker-compose.yml");
|
|
2023
2146
|
files.push(await pathExists(prefixedCore) ? prefixedCore : unprefixedCore);
|
|
2024
2147
|
for (const app of manifest.applications) {
|
|
2025
|
-
const prefixed =
|
|
2026
|
-
const unprefixed =
|
|
2148
|
+
const prefixed = join26(localDir, `${platformName}-${app.name}-docker-compose.yml`);
|
|
2149
|
+
const unprefixed = join26(localDir, `${app.name}-docker-compose.yml`);
|
|
2027
2150
|
if (await pathExists(prefixed)) files.push(prefixed);
|
|
2028
2151
|
else if (await pathExists(unprefixed)) files.push(unprefixed);
|
|
2029
2152
|
}
|
|
@@ -2129,15 +2252,15 @@ async function getServicesForComposeFiles(selectedFiles, allFiles, rootDir) {
|
|
|
2129
2252
|
async function checkContainersPerApp(layout, manifest, allContainers) {
|
|
2130
2253
|
const { localDir, coreDirName, rootDir } = layout;
|
|
2131
2254
|
const platformName = manifest.product.name;
|
|
2132
|
-
const prefixedCore =
|
|
2133
|
-
const unprefixedCore =
|
|
2255
|
+
const prefixedCore = join26(localDir, `${coreDirName}-docker-compose.yml`);
|
|
2256
|
+
const unprefixedCore = join26(localDir, "core-docker-compose.yml");
|
|
2134
2257
|
const coreComposePath = await pathExists(prefixedCore) ? prefixedCore : unprefixedCore;
|
|
2135
|
-
const platformComposePath =
|
|
2258
|
+
const platformComposePath = join26(localDir, "platform-docker-compose.yml");
|
|
2136
2259
|
const allFiles = [platformComposePath, coreComposePath];
|
|
2137
2260
|
const appComposeMap = /* @__PURE__ */ new Map();
|
|
2138
2261
|
for (const app of manifest.applications) {
|
|
2139
|
-
const prefixed =
|
|
2140
|
-
const unprefixed =
|
|
2262
|
+
const prefixed = join26(localDir, `${platformName}-${app.name}-docker-compose.yml`);
|
|
2263
|
+
const unprefixed = join26(localDir, `${app.name}-docker-compose.yml`);
|
|
2141
2264
|
if (await pathExists(prefixed)) {
|
|
2142
2265
|
appComposeMap.set(app.name, prefixed);
|
|
2143
2266
|
allFiles.push(prefixed);
|
|
@@ -2184,7 +2307,7 @@ async function checkContainersPerApp(layout, manifest, allContainers) {
|
|
|
2184
2307
|
return groups;
|
|
2185
2308
|
}
|
|
2186
2309
|
async function checkIdpProviders(layout) {
|
|
2187
|
-
const envPath =
|
|
2310
|
+
const envPath = join26(layout.localDir, ".env");
|
|
2188
2311
|
let env;
|
|
2189
2312
|
try {
|
|
2190
2313
|
env = await readEnvFile(envPath);
|
|
@@ -2303,7 +2426,7 @@ var statusCommand = {
|
|
|
2303
2426
|
};
|
|
2304
2427
|
|
|
2305
2428
|
// src/commands/manage-platform-admins/manage-platform-admins.command.ts
|
|
2306
|
-
import { join as
|
|
2429
|
+
import { join as join27 } from "path";
|
|
2307
2430
|
import { fetch as undiciFetch3, Agent as Agent3 } from "undici";
|
|
2308
2431
|
var MANAGE_PLATFORM_ADMINS_COMMAND_NAME = "manage-platform-admins";
|
|
2309
2432
|
var managePlatformAdminsCommand = {
|
|
@@ -2317,7 +2440,7 @@ async function getGatewayConfig(logger) {
|
|
|
2317
2440
|
logger.log("Error: Cannot manage platform admins \u2014 no platform initialized in this directory.");
|
|
2318
2441
|
return null;
|
|
2319
2442
|
}
|
|
2320
|
-
const envPath =
|
|
2443
|
+
const envPath = join27(layout.localDir, ".env");
|
|
2321
2444
|
let env;
|
|
2322
2445
|
try {
|
|
2323
2446
|
env = await readEnvFile(envPath);
|
|
@@ -2439,59 +2562,59 @@ async function removePlatformAdmin(ruleId, logger) {
|
|
|
2439
2562
|
var baPlatformPlugin = {
|
|
2440
2563
|
name: "ba-platform-plugin",
|
|
2441
2564
|
description: "Blue Alba Platform knowledge and CLI skills for AI assistants",
|
|
2442
|
-
version: "1.
|
|
2565
|
+
version: "1.2.0",
|
|
2443
2566
|
skills: [
|
|
2444
2567
|
{
|
|
2445
2568
|
name: "platform",
|
|
2446
|
-
description: "
|
|
2569
|
+
description: "Blue Alba Platform orchestrator \u2014 routes to the correct specialized skill or answers general architecture questions. Use this skill for ANY platform-related request.",
|
|
2447
2570
|
type: "skill",
|
|
2448
2571
|
sourceFile: "skills/ba-platform/platform.skill.md"
|
|
2449
2572
|
},
|
|
2450
2573
|
{
|
|
2451
2574
|
name: "platform-cli",
|
|
2452
|
-
description: "Blue Alba Platform CLI commands and
|
|
2575
|
+
description: "Blue Alba Platform CLI commands, usage, installation, and headless mode reference",
|
|
2453
2576
|
type: "skill",
|
|
2454
2577
|
sourceFile: "skills/ba-platform/platform-cli.skill.md"
|
|
2455
2578
|
},
|
|
2456
2579
|
{
|
|
2457
2580
|
name: "platform-add-operation",
|
|
2458
|
-
description: "
|
|
2581
|
+
description: "Adds an authorization operation (permission/RBAC guard) to a bootstrap service and enforces it in UI or backend routes",
|
|
2459
2582
|
type: "skill",
|
|
2460
2583
|
sourceFile: "skills/ba-platform/platform-add-operation.skill.md"
|
|
2461
2584
|
},
|
|
2462
2585
|
{
|
|
2463
2586
|
name: "platform-add-feature-flag",
|
|
2464
|
-
description: "Declares a feature flag in
|
|
2587
|
+
description: "Declares a feature flag in a bootstrap service and gates a React component or NestJS endpoint behind it",
|
|
2465
2588
|
type: "skill",
|
|
2466
2589
|
sourceFile: "skills/ba-platform/platform-add-feature-flag.skill.md"
|
|
2467
2590
|
},
|
|
2468
2591
|
{
|
|
2469
2592
|
name: "platform-add-scheduled-job",
|
|
2470
|
-
description: "Adds a scheduled job to a
|
|
2593
|
+
description: "Adds a cron-scheduled job to a bootstrap service to run a backend endpoint on a recurring schedule",
|
|
2471
2594
|
type: "skill",
|
|
2472
2595
|
sourceFile: "skills/ba-platform/platform-add-scheduled-job.skill.md"
|
|
2473
2596
|
},
|
|
2474
2597
|
{
|
|
2475
2598
|
name: "platform-add-menu-item",
|
|
2476
|
-
description: "
|
|
2599
|
+
description: "Creates a new page component and wires it as a menu item/route in a single-spa UI module",
|
|
2477
2600
|
type: "skill",
|
|
2478
2601
|
sourceFile: "skills/ba-platform/platform-add-menu-item.skill.md"
|
|
2479
2602
|
},
|
|
2480
2603
|
{
|
|
2481
2604
|
name: "platform-extend-shell",
|
|
2482
|
-
description: "Injects a custom component into a
|
|
2605
|
+
description: "Injects a custom component into a shell extension point (navbar, branding, user section)",
|
|
2483
2606
|
type: "skill",
|
|
2484
2607
|
sourceFile: "skills/ba-platform/platform-extend-shell.skill.md"
|
|
2485
2608
|
},
|
|
2486
2609
|
{
|
|
2487
2610
|
name: "platform-add-presence",
|
|
2488
|
-
description: "Adds real-time user presence
|
|
2611
|
+
description: "Adds real-time user presence (live avatars, who-is-viewing) to a component using the Rooms API",
|
|
2489
2612
|
type: "skill",
|
|
2490
2613
|
sourceFile: "skills/ba-platform/platform-add-presence.skill.md"
|
|
2491
2614
|
},
|
|
2492
2615
|
{
|
|
2493
2616
|
name: "platform-scaffold-module",
|
|
2494
|
-
description: "Scaffolds a new UI module or service module using the
|
|
2617
|
+
description: "Scaffolds a new UI module or NestJS service module in a platform monorepo using the CLI",
|
|
2495
2618
|
type: "skill",
|
|
2496
2619
|
sourceFile: "skills/ba-platform/platform-scaffold-module.skill.md"
|
|
2497
2620
|
}
|
|
@@ -2602,6 +2725,132 @@ var standaloneConfigListCommand = {
|
|
|
2602
2725
|
hidden: () => false
|
|
2603
2726
|
};
|
|
2604
2727
|
|
|
2728
|
+
// src/commands/pipelines/generate-github-pipeline.ts
|
|
2729
|
+
import { fileURLToPath as fileURLToPath10 } from "url";
|
|
2730
|
+
import { dirname as dirname13, join as join28 } from "path";
|
|
2731
|
+
|
|
2732
|
+
// src/commands/pipelines/github-secrets-checklist.ts
|
|
2733
|
+
var GITHUB_VARS_CHECKLIST = [
|
|
2734
|
+
"DOCKER_PUSH_TO_ACR \u2014 (variable) true/false, push to Azure Container Registry",
|
|
2735
|
+
"DOCKER_PUSH_TO_ECR \u2014 (variable) true/false, push to Amazon ECR",
|
|
2736
|
+
"DOCKER_PUSH_TO_BA_ARTIFACTORY \u2014 (variable) true/false, push to Blue Alba Artifactory",
|
|
2737
|
+
"DOCKER_REPOSITORY_URL \u2014 (variable) URL of the Docker registry",
|
|
2738
|
+
"DOCKER_IMAGE_ARCHS \u2014 (variable) target architectures (e.g. linux/amd64,linux/arm64)"
|
|
2739
|
+
];
|
|
2740
|
+
var GITHUB_SECRETS_CHECKLIST = [
|
|
2741
|
+
"DOCKER_USER \u2014 Docker registry username",
|
|
2742
|
+
"DOCKER_PASSWORD \u2014 Docker registry password",
|
|
2743
|
+
"NPM_AUTH_TOKEN \u2014 Blue Alba private npm registry token",
|
|
2744
|
+
"NPMJS_AUTH_TOKEN \u2014 npmjs.org registry token",
|
|
2745
|
+
"WORKFLOW_REPOSITORY_RO_SSH_KEY \u2014 read-only SSH key for Blue-Alba-Platform/workflows repo",
|
|
2746
|
+
"WORKFLOWS_STATE_ID_PAT \u2014 PAT for reading artifacts (state file) between branches"
|
|
2747
|
+
];
|
|
2748
|
+
|
|
2749
|
+
// src/commands/pipelines/generate-github-pipeline.ts
|
|
2750
|
+
var __dirname = dirname13(fileURLToPath10(import.meta.url));
|
|
2751
|
+
async function generateGithubPipeline(params, logger) {
|
|
2752
|
+
const templateDir4 = join28(__dirname, "..", "templates", "github-pipeline-template");
|
|
2753
|
+
await applyTemplate(
|
|
2754
|
+
{
|
|
2755
|
+
templateDir: templateDir4,
|
|
2756
|
+
outputDir: params.rootDir,
|
|
2757
|
+
variables: {
|
|
2758
|
+
stateFile: params.stateFile,
|
|
2759
|
+
codeFolderList: params.codeFolderList,
|
|
2760
|
+
packagesFolderPattern: params.packagesFolderPattern
|
|
2761
|
+
}
|
|
2762
|
+
},
|
|
2763
|
+
(message) => logger.log(message)
|
|
2764
|
+
);
|
|
2765
|
+
logger.log("");
|
|
2766
|
+
logger.log("GitHub Actions pipeline generated: .github/workflows/build-platform.yml");
|
|
2767
|
+
logger.log("");
|
|
2768
|
+
logger.log("Action required \u2014 configure the following in your GitHub repo (Settings > Secrets and variables):");
|
|
2769
|
+
logger.log("");
|
|
2770
|
+
logger.log("Variables:");
|
|
2771
|
+
for (const item of GITHUB_VARS_CHECKLIST) {
|
|
2772
|
+
logger.log(` \u2022 ${item}`);
|
|
2773
|
+
}
|
|
2774
|
+
logger.log("");
|
|
2775
|
+
logger.log("Secrets:");
|
|
2776
|
+
for (const item of GITHUB_SECRETS_CHECKLIST) {
|
|
2777
|
+
logger.log(` \u2022 ${item}`);
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
// src/commands/pipelines/generate-azure-pipeline.ts
|
|
2782
|
+
import { fileURLToPath as fileURLToPath11 } from "url";
|
|
2783
|
+
import { dirname as dirname14, join as join29 } from "path";
|
|
2784
|
+
var __dirname2 = dirname14(fileURLToPath11(import.meta.url));
|
|
2785
|
+
function buildServicesList(services) {
|
|
2786
|
+
if (services.length === 0) return "";
|
|
2787
|
+
return services.map((s) => ` - name: ${s.name}
|
|
2788
|
+
folder: ${s.folder}
|
|
2789
|
+
`).join("");
|
|
2790
|
+
}
|
|
2791
|
+
async function generateAzurePipeline(params, logger) {
|
|
2792
|
+
const templateDir4 = join29(__dirname2, "..", "templates", "azure-pipeline-template");
|
|
2793
|
+
const servicesList = buildServicesList(params.services);
|
|
2794
|
+
await applyTemplate(
|
|
2795
|
+
{
|
|
2796
|
+
templateDir: templateDir4,
|
|
2797
|
+
outputDir: params.rootDir,
|
|
2798
|
+
variables: {
|
|
2799
|
+
azureServiceConnection: params.azureServiceConnection,
|
|
2800
|
+
acrLoginServer: params.acrLoginServer,
|
|
2801
|
+
nodeVersion: params.nodeVersion,
|
|
2802
|
+
templatesRepoName: params.templatesRepoName,
|
|
2803
|
+
gitopsRepoName: params.gitopsRepoName,
|
|
2804
|
+
servicesList
|
|
2805
|
+
}
|
|
2806
|
+
},
|
|
2807
|
+
(message) => logger.log(message)
|
|
2808
|
+
);
|
|
2809
|
+
logger.log("Azure DevOps pipeline generated: .azuredevops/azure-pipeline.yml");
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
// src/commands/pipelines/pipelines.command.ts
|
|
2813
|
+
var PIPELINES_COMMAND_NAME = "pipelines";
|
|
2814
|
+
var pipelinesCommand = {
|
|
2815
|
+
name: PIPELINES_COMMAND_NAME,
|
|
2816
|
+
description: "Generate CI/CD pipeline files for the platform monorepo",
|
|
2817
|
+
hidden: (ctx) => !ctx.platformInitialized
|
|
2818
|
+
};
|
|
2819
|
+
async function generatePipelines(params, logger) {
|
|
2820
|
+
for (const target of params.targets) {
|
|
2821
|
+
logger.log(`
|
|
2822
|
+
Generating ${params.provider} pipeline for ${target.name}...`);
|
|
2823
|
+
if (params.provider === "github") {
|
|
2824
|
+
await generateGithubPipeline(
|
|
2825
|
+
{
|
|
2826
|
+
rootDir: target.dir,
|
|
2827
|
+
stateFile: params.stateFile ?? target.name,
|
|
2828
|
+
codeFolderList: params.codeFolderList ?? "apps/**,services/**",
|
|
2829
|
+
packagesFolderPattern: params.packagesFolderPattern ?? "packages/**"
|
|
2830
|
+
},
|
|
2831
|
+
logger
|
|
2832
|
+
);
|
|
2833
|
+
continue;
|
|
2834
|
+
}
|
|
2835
|
+
if (params.provider === "azure") {
|
|
2836
|
+
await generateAzurePipeline(
|
|
2837
|
+
{
|
|
2838
|
+
rootDir: target.dir,
|
|
2839
|
+
azureServiceConnection: params.azureServiceConnection ?? "",
|
|
2840
|
+
acrLoginServer: params.acrLoginServer ?? "",
|
|
2841
|
+
nodeVersion: params.nodeVersion ?? "22.x",
|
|
2842
|
+
templatesRepoName: params.templatesRepoName ?? "azure-pipeline-ba-template",
|
|
2843
|
+
gitopsRepoName: params.gitopsRepoName ?? "portainer-deployments",
|
|
2844
|
+
services: target.services ?? []
|
|
2845
|
+
},
|
|
2846
|
+
logger
|
|
2847
|
+
);
|
|
2848
|
+
continue;
|
|
2849
|
+
}
|
|
2850
|
+
logger.log(`Error: Unknown provider "${params.provider}". Use "github" or "azure".`);
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2605
2854
|
// src/commands/registry.ts
|
|
2606
2855
|
var CommandRegistry = class {
|
|
2607
2856
|
commands = /* @__PURE__ */ new Map();
|
|
@@ -2649,6 +2898,7 @@ registry.register(standaloneConfigShowCommand);
|
|
|
2649
2898
|
registry.register(standaloneConfigSetCommand);
|
|
2650
2899
|
registry.register(standaloneConfigDeleteCommand);
|
|
2651
2900
|
registry.register(standaloneConfigListCommand);
|
|
2901
|
+
registry.register(pipelinesCommand);
|
|
2652
2902
|
|
|
2653
2903
|
// src/app-state.ts
|
|
2654
2904
|
var APP_STATE = {
|
|
@@ -2711,6 +2961,145 @@ async function initService(params, logger) {
|
|
|
2711
2961
|
await init(params, logger);
|
|
2712
2962
|
}
|
|
2713
2963
|
|
|
2964
|
+
// src/services/pipelines.service.ts
|
|
2965
|
+
async function pipelinesService(params, logger) {
|
|
2966
|
+
await generatePipelines(params, logger);
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
// src/commands/pipelines/build-target-list.ts
|
|
2970
|
+
import { resolve as resolve8, join as join30 } from "path";
|
|
2971
|
+
function buildTargetList(rootDir, coreDirName, manifest) {
|
|
2972
|
+
const targets = [];
|
|
2973
|
+
targets.push({ name: coreDirName, dir: join30(rootDir, coreDirName) });
|
|
2974
|
+
for (const app of manifest.applications) {
|
|
2975
|
+
const appDir = resolve8(join30(rootDir, coreDirName, app.localPath));
|
|
2976
|
+
targets.push({ name: app.name, dir: appDir });
|
|
2977
|
+
}
|
|
2978
|
+
return targets;
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
// src/commands/pipelines/detect-services.ts
|
|
2982
|
+
import { readdir as readdir5, access as access10 } from "fs/promises";
|
|
2983
|
+
import { join as join31, relative, resolve as resolve9 } from "path";
|
|
2984
|
+
async function detectServicesInDir(targetDir) {
|
|
2985
|
+
const detected = [];
|
|
2986
|
+
for (const subDir of ["services", "ui"]) {
|
|
2987
|
+
const scanDir = join31(targetDir, subDir);
|
|
2988
|
+
try {
|
|
2989
|
+
await access10(scanDir);
|
|
2990
|
+
} catch {
|
|
2991
|
+
continue;
|
|
2992
|
+
}
|
|
2993
|
+
let dirNames;
|
|
2994
|
+
try {
|
|
2995
|
+
const dirents = await readdir5(scanDir, { withFileTypes: true });
|
|
2996
|
+
dirNames = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
2997
|
+
} catch {
|
|
2998
|
+
continue;
|
|
2999
|
+
}
|
|
3000
|
+
for (const dirName of dirNames) {
|
|
3001
|
+
const dockerfilePath = join31(scanDir, dirName, "Dockerfile");
|
|
3002
|
+
try {
|
|
3003
|
+
await access10(dockerfilePath);
|
|
3004
|
+
const folder = relative(targetDir, join31(scanDir, dirName)).replace(/\\/g, "/");
|
|
3005
|
+
detected.push({ name: dirName, folder });
|
|
3006
|
+
} catch {
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
return detected;
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
// src/controllers/ui/pipelines.ui-controller.ts
|
|
3014
|
+
async function pipelinesUiController(ctx) {
|
|
3015
|
+
if (!await isPlatformInitialized()) {
|
|
3016
|
+
ctx.log("Error: Cannot configure pipelines \u2014 no platform initialized in this directory.");
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
const layout = await findPlatformLayout();
|
|
3020
|
+
if (!layout) return;
|
|
3021
|
+
const { rootDir, coreDirName } = layout;
|
|
3022
|
+
let manifest;
|
|
3023
|
+
try {
|
|
3024
|
+
manifest = await readManifest(rootDir, coreDirName);
|
|
3025
|
+
} catch {
|
|
3026
|
+
ctx.log("Error: Could not read product manifest.");
|
|
3027
|
+
return;
|
|
3028
|
+
}
|
|
3029
|
+
const allTargets = buildTargetList(rootDir, coreDirName, manifest);
|
|
3030
|
+
const targetOptions = allTargets.map((t) => ({
|
|
3031
|
+
label: t.name === coreDirName ? `${t.name} (core)` : t.name,
|
|
3032
|
+
value: t.name
|
|
3033
|
+
}));
|
|
3034
|
+
const selectedNames = await ctx.multiselect(
|
|
3035
|
+
"Select repositories to generate pipelines for:",
|
|
3036
|
+
targetOptions
|
|
3037
|
+
);
|
|
3038
|
+
if (selectedNames.length === 0) {
|
|
3039
|
+
ctx.log("No targets selected. Nothing to do.");
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
const selectedTargets = allTargets.filter((t) => selectedNames.includes(t.name));
|
|
3043
|
+
const provider = await ctx.select("CI/CD provider:", [
|
|
3044
|
+
{ label: "GitHub Actions", value: "github" },
|
|
3045
|
+
{ label: "Azure DevOps", value: "azure" }
|
|
3046
|
+
]);
|
|
3047
|
+
if (provider === "github") {
|
|
3048
|
+
await pipelinesService(
|
|
3049
|
+
{
|
|
3050
|
+
provider: "github",
|
|
3051
|
+
targets: selectedTargets,
|
|
3052
|
+
codeFolderList: "apps/**,services/**",
|
|
3053
|
+
packagesFolderPattern: "packages/**"
|
|
3054
|
+
},
|
|
3055
|
+
ctx
|
|
3056
|
+
);
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
const azureServiceConnection = await ctx.prompt("Azure Service Connection name:");
|
|
3060
|
+
if (!azureServiceConnection.trim()) {
|
|
3061
|
+
ctx.log("Error: Azure Service Connection name is required.");
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
3064
|
+
const acrLoginServer = await ctx.prompt("ACR Login Server (e.g. myregistry.azurecr.io):");
|
|
3065
|
+
if (!acrLoginServer.trim()) {
|
|
3066
|
+
ctx.log("Error: ACR Login Server is required.");
|
|
3067
|
+
return;
|
|
3068
|
+
}
|
|
3069
|
+
const targetsWithServices = [];
|
|
3070
|
+
for (const target of selectedTargets) {
|
|
3071
|
+
const detected = await detectServicesInDir(target.dir);
|
|
3072
|
+
let services;
|
|
3073
|
+
if (detected.length === 0) {
|
|
3074
|
+
ctx.log(` No services with Dockerfile found in ${target.name} \u2014 pipeline will have an empty services list.`);
|
|
3075
|
+
services = [];
|
|
3076
|
+
} else {
|
|
3077
|
+
const options = detected.map((s) => ({ label: `${s.name} (${s.folder})`, value: s.folder }));
|
|
3078
|
+
const selectedFolders = await ctx.multiselect(
|
|
3079
|
+
`Services to include for ${target.name}:`,
|
|
3080
|
+
options
|
|
3081
|
+
);
|
|
3082
|
+
services = selectedFolders.map((folder) => {
|
|
3083
|
+
const svc = detected.find((d) => d.folder === folder);
|
|
3084
|
+
return { name: svc.name, folder: svc.folder };
|
|
3085
|
+
});
|
|
3086
|
+
}
|
|
3087
|
+
targetsWithServices.push({ ...target, services });
|
|
3088
|
+
}
|
|
3089
|
+
await pipelinesService(
|
|
3090
|
+
{
|
|
3091
|
+
provider: "azure",
|
|
3092
|
+
targets: targetsWithServices,
|
|
3093
|
+
azureServiceConnection,
|
|
3094
|
+
acrLoginServer,
|
|
3095
|
+
nodeVersion: "22.x",
|
|
3096
|
+
templatesRepoName: "azure-pipeline-ba-template",
|
|
3097
|
+
gitopsRepoName: "portainer-deployments"
|
|
3098
|
+
},
|
|
3099
|
+
ctx
|
|
3100
|
+
);
|
|
3101
|
+
}
|
|
3102
|
+
|
|
2714
3103
|
// src/controllers/ui/init.ui-controller.ts
|
|
2715
3104
|
async function initUiController(ctx) {
|
|
2716
3105
|
if (await isPlatformInitialized()) {
|
|
@@ -2746,6 +3135,10 @@ async function initUiController(ctx) {
|
|
|
2746
3135
|
{ organizationName, platformName, platformDisplayName, bootstrapServiceSuffix, customizationUiSuffix },
|
|
2747
3136
|
ctx
|
|
2748
3137
|
);
|
|
3138
|
+
const addPipelines = await ctx.confirm("Configure CI/CD pipelines?", false);
|
|
3139
|
+
if (addPipelines) {
|
|
3140
|
+
await pipelinesUiController(ctx);
|
|
3141
|
+
}
|
|
2749
3142
|
}
|
|
2750
3143
|
|
|
2751
3144
|
// src/services/configure-idp.service.ts
|
|
@@ -3180,20 +3573,20 @@ async function installAiPluginService(params, context) {
|
|
|
3180
3573
|
|
|
3181
3574
|
// src/commands/install-ai-plugin/providers/claude.provider.ts
|
|
3182
3575
|
import { homedir as homedir3 } from "os";
|
|
3183
|
-
import { join as
|
|
3184
|
-
import { readFile as
|
|
3576
|
+
import { join as join32 } from "path";
|
|
3577
|
+
import { readFile as readFile13, writeFile as writeFile13, mkdir as mkdir4, rm } from "fs/promises";
|
|
3185
3578
|
var ClaudeProvider = class {
|
|
3186
3579
|
name = "claude";
|
|
3187
|
-
baseDir =
|
|
3580
|
+
baseDir = join32(homedir3(), ".claude");
|
|
3188
3581
|
getInstallPath(skillName) {
|
|
3189
|
-
return
|
|
3582
|
+
return join32(this.baseDir, "skills", skillName, "SKILL.md");
|
|
3190
3583
|
}
|
|
3191
3584
|
manifestPath(pluginName) {
|
|
3192
|
-
return
|
|
3585
|
+
return join32(this.baseDir, `${pluginName}.manifest.json`);
|
|
3193
3586
|
}
|
|
3194
3587
|
async readManifest(pluginName) {
|
|
3195
3588
|
try {
|
|
3196
|
-
const content = await
|
|
3589
|
+
const content = await readFile13(this.manifestPath(pluginName), "utf-8");
|
|
3197
3590
|
return JSON.parse(content);
|
|
3198
3591
|
} catch {
|
|
3199
3592
|
return null;
|
|
@@ -3201,7 +3594,7 @@ var ClaudeProvider = class {
|
|
|
3201
3594
|
}
|
|
3202
3595
|
async writeManifest(manifest) {
|
|
3203
3596
|
await mkdir4(this.baseDir, { recursive: true });
|
|
3204
|
-
await
|
|
3597
|
+
await writeFile13(
|
|
3205
3598
|
this.manifestPath(manifest.plugin),
|
|
3206
3599
|
JSON.stringify(manifest, null, 2),
|
|
3207
3600
|
"utf-8"
|
|
@@ -3221,16 +3614,16 @@ var ClaudeProvider = class {
|
|
|
3221
3614
|
}
|
|
3222
3615
|
async installSkill(skill, docsSource, logger) {
|
|
3223
3616
|
const sourcePath = docsSource.resolve(skill.sourceFile);
|
|
3224
|
-
let content = await
|
|
3617
|
+
let content = await readFile13(sourcePath, "utf-8");
|
|
3225
3618
|
const docsPath = docsSource.resolve("docs");
|
|
3226
3619
|
content = content.replaceAll("{{docsPath}}", docsPath);
|
|
3227
3620
|
const installPath = this.getInstallPath(skill.name);
|
|
3228
|
-
await mkdir4(
|
|
3229
|
-
await
|
|
3621
|
+
await mkdir4(join32(installPath, ".."), { recursive: true });
|
|
3622
|
+
await writeFile13(installPath, content, "utf-8");
|
|
3230
3623
|
logger.log(` Installed skill: ${installPath}`);
|
|
3231
3624
|
}
|
|
3232
3625
|
async removeSkill(skillName, logger) {
|
|
3233
|
-
const skillDir =
|
|
3626
|
+
const skillDir = join32(this.baseDir, "skills", skillName);
|
|
3234
3627
|
try {
|
|
3235
3628
|
await rm(skillDir, { recursive: true, force: true });
|
|
3236
3629
|
logger.log(` Removed skill: ${skillDir}`);
|
|
@@ -3238,11 +3631,11 @@ var ClaudeProvider = class {
|
|
|
3238
3631
|
}
|
|
3239
3632
|
}
|
|
3240
3633
|
async updatePermissions(docsSource, logger) {
|
|
3241
|
-
const settingsPath =
|
|
3634
|
+
const settingsPath = join32(this.baseDir, "settings.json");
|
|
3242
3635
|
const docsPath = docsSource.resolve("docs");
|
|
3243
3636
|
let settings = {};
|
|
3244
3637
|
try {
|
|
3245
|
-
const content = await
|
|
3638
|
+
const content = await readFile13(settingsPath, "utf-8");
|
|
3246
3639
|
settings = JSON.parse(content);
|
|
3247
3640
|
} catch {
|
|
3248
3641
|
}
|
|
@@ -3254,7 +3647,7 @@ var ClaudeProvider = class {
|
|
|
3254
3647
|
permissions.additionalDirectories = [...additionalDirectories, docsPath];
|
|
3255
3648
|
settings.permissions = permissions;
|
|
3256
3649
|
await mkdir4(this.baseDir, { recursive: true });
|
|
3257
|
-
await
|
|
3650
|
+
await writeFile13(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
3258
3651
|
logger.log(` Granted read access to docs: ${docsPath}`);
|
|
3259
3652
|
}
|
|
3260
3653
|
};
|
|
@@ -3273,12 +3666,12 @@ function getProvider(name) {
|
|
|
3273
3666
|
}
|
|
3274
3667
|
|
|
3275
3668
|
// src/commands/install-ai-plugin/docs-source/local.docs-source.ts
|
|
3276
|
-
import { fileURLToPath as
|
|
3277
|
-
import { join as
|
|
3278
|
-
var packageRoot =
|
|
3669
|
+
import { fileURLToPath as fileURLToPath12 } from "url";
|
|
3670
|
+
import { join as join33, dirname as dirname15 } from "path";
|
|
3671
|
+
var packageRoot = join33(dirname15(fileURLToPath12(import.meta.url)), "..");
|
|
3279
3672
|
var LocalDocsSource = class {
|
|
3280
3673
|
resolve(relativePath) {
|
|
3281
|
-
return
|
|
3674
|
+
return join33(packageRoot, relativePath);
|
|
3282
3675
|
}
|
|
3283
3676
|
};
|
|
3284
3677
|
|
|
@@ -3322,20 +3715,20 @@ async function installAiPluginUiController(ctx) {
|
|
|
3322
3715
|
|
|
3323
3716
|
// src/controllers/ui/standalone.ui-controller.ts
|
|
3324
3717
|
import { rm as rm3 } from "fs/promises";
|
|
3325
|
-
import { join as
|
|
3718
|
+
import { join as join37 } from "path";
|
|
3326
3719
|
import { cwd as cwd7 } from "process";
|
|
3327
3720
|
import { exec } from "child_process";
|
|
3328
3721
|
|
|
3329
3722
|
// src/commands/standalone/app-monorepo-check.ts
|
|
3330
|
-
import { access as
|
|
3331
|
-
import { join as
|
|
3723
|
+
import { access as access11, readdir as readdir6 } from "fs/promises";
|
|
3724
|
+
import { join as join34 } from "path";
|
|
3332
3725
|
import { cwd as cwd6 } from "process";
|
|
3333
3726
|
async function findAppMonorepoLayout(startDir = cwd6()) {
|
|
3334
|
-
const composePath =
|
|
3335
|
-
const packageJsonPath =
|
|
3727
|
+
const composePath = join34(startDir, "docker-compose.yml");
|
|
3728
|
+
const packageJsonPath = join34(startDir, "package.json");
|
|
3336
3729
|
try {
|
|
3337
|
-
await
|
|
3338
|
-
await
|
|
3730
|
+
await access11(composePath);
|
|
3731
|
+
await access11(packageJsonPath);
|
|
3339
3732
|
} catch {
|
|
3340
3733
|
return null;
|
|
3341
3734
|
}
|
|
@@ -3346,11 +3739,11 @@ async function findAppMonorepoLayout(startDir = cwd6()) {
|
|
|
3346
3739
|
}
|
|
3347
3740
|
async function hasCoreChildDir(dir) {
|
|
3348
3741
|
try {
|
|
3349
|
-
const entries = await
|
|
3742
|
+
const entries = await readdir6(dir, { withFileTypes: true });
|
|
3350
3743
|
for (const entry of entries) {
|
|
3351
3744
|
if (entry.isDirectory() && (entry.name.endsWith("-core") || entry.name === "core")) {
|
|
3352
3745
|
try {
|
|
3353
|
-
await
|
|
3746
|
+
await access11(join34(dir, entry.name, "product.manifest.json"));
|
|
3354
3747
|
return true;
|
|
3355
3748
|
} catch {
|
|
3356
3749
|
}
|
|
@@ -3365,36 +3758,36 @@ async function isInAppMonorepo() {
|
|
|
3365
3758
|
}
|
|
3366
3759
|
|
|
3367
3760
|
// src/commands/standalone/standalone-auto-detect.ts
|
|
3368
|
-
import { readdir as
|
|
3369
|
-
import { join as
|
|
3761
|
+
import { readdir as readdir7, readFile as readFile14 } from "fs/promises";
|
|
3762
|
+
import { join as join35, basename as basename2 } from "path";
|
|
3370
3763
|
async function autoDetectAppIdentity(appDir) {
|
|
3371
3764
|
const fromBootstrap = await detectFromBootstrap(appDir);
|
|
3372
3765
|
if (fromBootstrap) return fromBootstrap;
|
|
3373
3766
|
return detectFromPackageJson(appDir);
|
|
3374
3767
|
}
|
|
3375
3768
|
async function detectFromBootstrap(appDir) {
|
|
3376
|
-
const servicesDir =
|
|
3769
|
+
const servicesDir = join35(appDir, "services");
|
|
3377
3770
|
let entries;
|
|
3378
3771
|
try {
|
|
3379
|
-
entries = await
|
|
3772
|
+
entries = await readdir7(servicesDir);
|
|
3380
3773
|
} catch {
|
|
3381
3774
|
return null;
|
|
3382
3775
|
}
|
|
3383
3776
|
const bootstrapDirs = entries.filter((e) => e.endsWith("-bootstrap-service"));
|
|
3384
3777
|
if (bootstrapDirs.length === 0) return null;
|
|
3385
3778
|
for (const bootstrapDirName of bootstrapDirs) {
|
|
3386
|
-
const dataDir =
|
|
3779
|
+
const dataDir = join35(servicesDir, bootstrapDirName, "src", "data");
|
|
3387
3780
|
let dataDirEntries;
|
|
3388
3781
|
try {
|
|
3389
|
-
dataDirEntries = await
|
|
3782
|
+
dataDirEntries = await readdir7(dataDir);
|
|
3390
3783
|
} catch {
|
|
3391
3784
|
continue;
|
|
3392
3785
|
}
|
|
3393
3786
|
for (const subDir of dataDirEntries) {
|
|
3394
3787
|
if (subDir === "platform") continue;
|
|
3395
|
-
const appJsonPath =
|
|
3788
|
+
const appJsonPath = join35(dataDir, subDir, "application.json");
|
|
3396
3789
|
try {
|
|
3397
|
-
const content = await
|
|
3790
|
+
const content = await readFile14(appJsonPath, "utf-8");
|
|
3398
3791
|
const appData = JSON.parse(content);
|
|
3399
3792
|
if (!appData.name) continue;
|
|
3400
3793
|
const applicationName = appData.name;
|
|
@@ -3412,7 +3805,7 @@ async function detectFromBootstrap(appDir) {
|
|
|
3412
3805
|
}
|
|
3413
3806
|
async function detectFromPackageJson(appDir) {
|
|
3414
3807
|
try {
|
|
3415
|
-
const content = await
|
|
3808
|
+
const content = await readFile14(join35(appDir, "package.json"), "utf-8");
|
|
3416
3809
|
const pkg = JSON.parse(content);
|
|
3417
3810
|
const rawName = pkg.name ?? "";
|
|
3418
3811
|
const applicationName = rawName.includes("/") ? rawName.split("/")[1] ?? rawName : rawName;
|
|
@@ -3435,23 +3828,24 @@ function inferPlatformNameFromAppDir(appDir, applicationName) {
|
|
|
3435
3828
|
}
|
|
3436
3829
|
|
|
3437
3830
|
// src/commands/standalone/standalone-orchestrator.ts
|
|
3438
|
-
import { join as
|
|
3439
|
-
import { mkdir as mkdir5, rm as rm2, readdir as
|
|
3831
|
+
import { join as join36, dirname as dirname16 } from "path";
|
|
3832
|
+
import { mkdir as mkdir5, rm as rm2, readdir as readdir8, access as access12, writeFile as writeFile14, readFile as readFile15, open } from "fs/promises";
|
|
3833
|
+
import { constants } from "fs";
|
|
3440
3834
|
import { fetch as undiciFetch4, Agent as Agent4 } from "undici";
|
|
3441
3835
|
var TMP_BASE = "/tmp";
|
|
3442
3836
|
var STANDALONE_PREFIX = "platform-standalone-";
|
|
3443
3837
|
var DETACH_MARKER = ".standalone-running";
|
|
3444
3838
|
function getTmpDir(platformName) {
|
|
3445
|
-
return
|
|
3839
|
+
return join36(TMP_BASE, `${STANDALONE_PREFIX}${platformName}`);
|
|
3446
3840
|
}
|
|
3447
3841
|
async function findRunningSingleton() {
|
|
3448
3842
|
try {
|
|
3449
|
-
const entries = await
|
|
3843
|
+
const entries = await readdir8(TMP_BASE, { withFileTypes: true });
|
|
3450
3844
|
for (const entry of entries) {
|
|
3451
3845
|
if (entry.isDirectory() && entry.name.startsWith(STANDALONE_PREFIX)) {
|
|
3452
|
-
const candidateDir =
|
|
3846
|
+
const candidateDir = join36(TMP_BASE, entry.name);
|
|
3453
3847
|
try {
|
|
3454
|
-
await
|
|
3848
|
+
await access12(join36(candidateDir, DETACH_MARKER));
|
|
3455
3849
|
return candidateDir;
|
|
3456
3850
|
} catch {
|
|
3457
3851
|
}
|
|
@@ -3495,7 +3889,7 @@ async function configureIdpFromConfig(config, localDir, logger) {
|
|
|
3495
3889
|
logger.log("No IDP configured \u2014 run 'platform standalone-config set' to add an IDP. Skipping IDP setup.");
|
|
3496
3890
|
return;
|
|
3497
3891
|
}
|
|
3498
|
-
const envPath =
|
|
3892
|
+
const envPath = join36(localDir, ".env");
|
|
3499
3893
|
let env;
|
|
3500
3894
|
try {
|
|
3501
3895
|
env = await readEnvFile(envPath);
|
|
@@ -3550,7 +3944,7 @@ async function configureAdminUsersFromConfig(config, localDir, logger) {
|
|
|
3550
3944
|
if (!config.adminUsers || config.adminUsers.length === 0) {
|
|
3551
3945
|
return;
|
|
3552
3946
|
}
|
|
3553
|
-
const envPath =
|
|
3947
|
+
const envPath = join36(localDir, ".env");
|
|
3554
3948
|
let env;
|
|
3555
3949
|
try {
|
|
3556
3950
|
env = await readEnvFile(envPath);
|
|
@@ -3593,19 +3987,19 @@ async function configureAdminUsersFromConfig(config, localDir, logger) {
|
|
|
3593
3987
|
}
|
|
3594
3988
|
}
|
|
3595
3989
|
function buildLayout(tmpDir, coreDirName) {
|
|
3596
|
-
const coreDir =
|
|
3990
|
+
const coreDir = join36(tmpDir, coreDirName);
|
|
3597
3991
|
return {
|
|
3598
3992
|
rootDir: tmpDir,
|
|
3599
3993
|
coreDir,
|
|
3600
3994
|
coreDirName,
|
|
3601
|
-
localDir:
|
|
3995
|
+
localDir: join36(coreDir, "local")
|
|
3602
3996
|
};
|
|
3603
3997
|
}
|
|
3604
|
-
async function
|
|
3605
|
-
const appComposePath =
|
|
3998
|
+
async function generateProxyAppCompose2(appDir, localDir, platformName, applicationName, logger) {
|
|
3999
|
+
const appComposePath = join36(appDir, "docker-compose.yml");
|
|
3606
4000
|
let content;
|
|
3607
4001
|
try {
|
|
3608
|
-
content = await
|
|
4002
|
+
content = await readFile15(appComposePath, "utf-8");
|
|
3609
4003
|
} catch {
|
|
3610
4004
|
logger.log(`Warning: No docker-compose.yml found at ${appComposePath} \u2014 app services won't start.`);
|
|
3611
4005
|
return;
|
|
@@ -3614,10 +4008,10 @@ async function generateProxyAppCompose(appDir, localDir, platformName, applicati
|
|
|
3614
4008
|
/^(\s+context:\s+)\.\/(.*)$/gm,
|
|
3615
4009
|
`$1${appDir}/$2`
|
|
3616
4010
|
);
|
|
3617
|
-
const appParentDir =
|
|
4011
|
+
const appParentDir = dirname16(appDir);
|
|
3618
4012
|
const rewritten = withAbsContexts.replace(/\$\{PWD\}/g, appParentDir);
|
|
3619
|
-
const proxyPath =
|
|
3620
|
-
await
|
|
4013
|
+
const proxyPath = join36(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
|
|
4014
|
+
await writeFile14(proxyPath, rewritten, "utf-8");
|
|
3621
4015
|
logger.log(`Generated app compose with absolute build paths.`);
|
|
3622
4016
|
}
|
|
3623
4017
|
function buildComposeManifest(manifest) {
|
|
@@ -3643,7 +4037,23 @@ async function startStandalone(config, appDir, logger, signal, detach = false) {
|
|
|
3643
4037
|
}
|
|
3644
4038
|
logger.log(`Starting standalone platform for "${applicationDisplayName}"...`);
|
|
3645
4039
|
await mkdir5(tmpDir, { recursive: true });
|
|
3646
|
-
|
|
4040
|
+
let markerFd;
|
|
4041
|
+
try {
|
|
4042
|
+
markerFd = await open(
|
|
4043
|
+
join36(tmpDir, DETACH_MARKER),
|
|
4044
|
+
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY
|
|
4045
|
+
);
|
|
4046
|
+
await markerFd.writeFile(platformName, "utf-8");
|
|
4047
|
+
} catch (err) {
|
|
4048
|
+
if (err.code === "EEXIST") {
|
|
4049
|
+
logger.log(`Error: A standalone platform is already starting at ${tmpDir}.`);
|
|
4050
|
+
logger.log(`Run "platform standalone-stop" to stop it before starting a new one.`);
|
|
4051
|
+
return;
|
|
4052
|
+
}
|
|
4053
|
+
throw err;
|
|
4054
|
+
} finally {
|
|
4055
|
+
await markerFd?.close();
|
|
4056
|
+
}
|
|
3647
4057
|
const variables = {
|
|
3648
4058
|
organizationName,
|
|
3649
4059
|
platformName,
|
|
@@ -3668,9 +4078,9 @@ async function startStandalone(config, appDir, logger, signal, detach = false) {
|
|
|
3668
4078
|
}
|
|
3669
4079
|
);
|
|
3670
4080
|
await writeManifest(npmManifest, tmpDir, coreDirName);
|
|
3671
|
-
const bootstrapServiceDir =
|
|
4081
|
+
const bootstrapServiceDir = join36(tmpDir, coreDirName, "services", bootstrapServiceName);
|
|
3672
4082
|
await scaffoldPlatformBootstrap(bootstrapServiceDir, variables, logger);
|
|
3673
|
-
const customizationUiDir =
|
|
4083
|
+
const customizationUiDir = join36(tmpDir, coreDirName, "ui", customizationUiName);
|
|
3674
4084
|
await scaffoldCustomizationUi(customizationUiDir, variables, logger);
|
|
3675
4085
|
await registerCustomizationModule(bootstrapServiceDir, organizationName, logger, platformName);
|
|
3676
4086
|
const layout = buildLayout(tmpDir, coreDirName);
|
|
@@ -3680,7 +4090,7 @@ async function startStandalone(config, appDir, logger, signal, detach = false) {
|
|
|
3680
4090
|
logger.log("Building...");
|
|
3681
4091
|
await buildAll(layout, npmManifest, logger, signal, true);
|
|
3682
4092
|
if (signal?.aborted) return;
|
|
3683
|
-
await
|
|
4093
|
+
await generateProxyAppCompose2(appDir, layout.localDir, platformName, applicationName, logger);
|
|
3684
4094
|
const composeManifest = buildComposeManifest(npmManifest);
|
|
3685
4095
|
await writeManifest(composeManifest, tmpDir, coreDirName);
|
|
3686
4096
|
logger.log("Resetting previous environment (removing stale containers and volumes)...");
|
|
@@ -3689,7 +4099,7 @@ async function startStandalone(config, appDir, logger, signal, detach = false) {
|
|
|
3689
4099
|
logger.log("Starting containers...");
|
|
3690
4100
|
await startEnvironment(layout, composeManifest, logger, signal);
|
|
3691
4101
|
if (signal?.aborted) return;
|
|
3692
|
-
const env = await readEnvFile(
|
|
4102
|
+
const env = await readEnvFile(join36(layout.localDir, ".env")).catch(() => /* @__PURE__ */ new Map());
|
|
3693
4103
|
const gatewayUrl = env.get("PAE_GATEWAY_HOST_URL") ?? "https://localhost:443";
|
|
3694
4104
|
const gatewayReady = await waitForGateway(gatewayUrl, logger);
|
|
3695
4105
|
if (!gatewayReady) {
|
|
@@ -3709,16 +4119,27 @@ Standalone platform is running in the background.`);
|
|
|
3709
4119
|
logger.log("\nPlatform is running. Press Ctrl+C to stop and clean up.");
|
|
3710
4120
|
const keepAlive = setInterval(() => {
|
|
3711
4121
|
}, 60 * 6e4);
|
|
4122
|
+
let cleanupInProgress = false;
|
|
3712
4123
|
const shutdown = () => {
|
|
4124
|
+
if (cleanupInProgress) {
|
|
4125
|
+
logger.log("\nForce exiting. Containers may need manual cleanup:");
|
|
4126
|
+
logger.log(` docker compose -p ${platformName}-platform down -v --remove-orphans`);
|
|
4127
|
+
process.exit(1);
|
|
4128
|
+
}
|
|
4129
|
+
cleanupInProgress = true;
|
|
3713
4130
|
clearInterval(keepAlive);
|
|
3714
|
-
|
|
4131
|
+
(async () => {
|
|
3715
4132
|
logger.log("\nShutting down standalone platform...");
|
|
3716
4133
|
await destroyEnvironment(layout, composeManifest, logger);
|
|
3717
4134
|
await cleanupTmpDir(tmpDir, logger);
|
|
3718
|
-
|
|
4135
|
+
process.exit(0);
|
|
4136
|
+
})().catch((err) => {
|
|
4137
|
+
logger.log(`Error during cleanup: ${err.message}`);
|
|
4138
|
+
process.exit(1);
|
|
4139
|
+
});
|
|
3719
4140
|
};
|
|
3720
|
-
process.
|
|
3721
|
-
process.
|
|
4141
|
+
process.on("SIGINT", shutdown);
|
|
4142
|
+
process.on("SIGTERM", shutdown);
|
|
3722
4143
|
}
|
|
3723
4144
|
async function stopStandalone(logger) {
|
|
3724
4145
|
const runningDir = await findRunningSingleton();
|
|
@@ -3728,11 +4149,11 @@ async function stopStandalone(logger) {
|
|
|
3728
4149
|
}
|
|
3729
4150
|
let coreDirName = null;
|
|
3730
4151
|
try {
|
|
3731
|
-
const entries = await
|
|
4152
|
+
const entries = await readdir8(runningDir, { withFileTypes: true });
|
|
3732
4153
|
for (const entry of entries) {
|
|
3733
4154
|
if (entry.isDirectory() && entry.name.endsWith("-core")) {
|
|
3734
4155
|
try {
|
|
3735
|
-
await
|
|
4156
|
+
await access12(join36(runningDir, entry.name, "product.manifest.json"));
|
|
3736
4157
|
coreDirName = entry.name;
|
|
3737
4158
|
break;
|
|
3738
4159
|
} catch {
|
|
@@ -3769,9 +4190,9 @@ async function stopStandalone(logger) {
|
|
|
3769
4190
|
|
|
3770
4191
|
// src/controllers/ui/standalone.ui-controller.ts
|
|
3771
4192
|
function getGitEmail() {
|
|
3772
|
-
return new Promise((
|
|
4193
|
+
return new Promise((resolve11) => {
|
|
3773
4194
|
exec("git config user.email", (err, stdout) => {
|
|
3774
|
-
|
|
4195
|
+
resolve11(err ? "" : stdout.trim());
|
|
3775
4196
|
});
|
|
3776
4197
|
});
|
|
3777
4198
|
}
|
|
@@ -3847,7 +4268,7 @@ async function standaloneUiController(ctx) {
|
|
|
3847
4268
|
const deleteLegacy = await ctx.confirm("Delete the local standalone.json?", true);
|
|
3848
4269
|
if (deleteLegacy) {
|
|
3849
4270
|
try {
|
|
3850
|
-
await rm3(
|
|
4271
|
+
await rm3(join37(appDir, "standalone.json"));
|
|
3851
4272
|
ctx.log("Deleted standalone.json.");
|
|
3852
4273
|
} catch {
|
|
3853
4274
|
ctx.log("Warning: Could not delete standalone.json.");
|
|
@@ -3979,7 +4400,7 @@ async function standaloneConfigShowUiController(ctx) {
|
|
|
3979
4400
|
if (config) {
|
|
3980
4401
|
ctx.log(`Standalone config for ${detected.platformName}/${detected.applicationName}:
|
|
3981
4402
|
`);
|
|
3982
|
-
ctx.log(JSON.stringify(config, null, 2));
|
|
4403
|
+
ctx.log(JSON.stringify(maskSensitiveFields(config), null, 2));
|
|
3983
4404
|
} else {
|
|
3984
4405
|
ctx.log(`No config found for ${detected.platformName}/${detected.applicationName}.`);
|
|
3985
4406
|
ctx.log(`Run 'platform standalone' or 'platform standalone-config set' to configure.`);
|
|
@@ -3998,7 +4419,7 @@ async function showAll(ctx) {
|
|
|
3998
4419
|
if (Object.keys(defaults).length === 0) {
|
|
3999
4420
|
ctx.log("(none)");
|
|
4000
4421
|
} else {
|
|
4001
|
-
ctx.log(JSON.stringify(defaults, null, 2));
|
|
4422
|
+
ctx.log(JSON.stringify(maskSensitiveFields(defaults), null, 2));
|
|
4002
4423
|
}
|
|
4003
4424
|
ctx.log("\n--- Apps ---");
|
|
4004
4425
|
if (apps.length === 0) {
|
|
@@ -4130,9 +4551,9 @@ async function collectIdpConfig2(ctx, current) {
|
|
|
4130
4551
|
return { provider: providerType, name, issuer, clientId, clientSecret, extras };
|
|
4131
4552
|
}
|
|
4132
4553
|
function getGitEmail2() {
|
|
4133
|
-
return new Promise((
|
|
4554
|
+
return new Promise((resolve11) => {
|
|
4134
4555
|
exec2("git config user.email", (err, stdout) => {
|
|
4135
|
-
|
|
4556
|
+
resolve11(err ? "" : stdout.trim());
|
|
4136
4557
|
});
|
|
4137
4558
|
});
|
|
4138
4559
|
}
|
|
@@ -4171,7 +4592,8 @@ var uiControllers = /* @__PURE__ */ new Map([
|
|
|
4171
4592
|
[STANDALONE_CONFIG_SHOW_COMMAND_NAME, standaloneConfigShowUiController],
|
|
4172
4593
|
[STANDALONE_CONFIG_SET_COMMAND_NAME, standaloneConfigSetUiController],
|
|
4173
4594
|
[STANDALONE_CONFIG_DELETE_COMMAND_NAME, standaloneConfigDeleteUiController],
|
|
4174
|
-
[STANDALONE_CONFIG_LIST_COMMAND_NAME, standaloneConfigListUiController]
|
|
4595
|
+
[STANDALONE_CONFIG_LIST_COMMAND_NAME, standaloneConfigListUiController],
|
|
4596
|
+
[PIPELINES_COMMAND_NAME, pipelinesUiController]
|
|
4175
4597
|
]);
|
|
4176
4598
|
|
|
4177
4599
|
// src/hooks/use-command-runner.ts
|
|
@@ -4205,7 +4627,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
|
|
|
4205
4627
|
}
|
|
4206
4628
|
},
|
|
4207
4629
|
prompt(message, defaultValue) {
|
|
4208
|
-
return new Promise((
|
|
4630
|
+
return new Promise((resolve11, reject) => {
|
|
4209
4631
|
if (controller.signal.aborted) {
|
|
4210
4632
|
reject(new DOMException("Aborted", "AbortError"));
|
|
4211
4633
|
return;
|
|
@@ -4213,7 +4635,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
|
|
|
4213
4635
|
setPromptMessage(message);
|
|
4214
4636
|
setPromptValue(defaultValue ?? "");
|
|
4215
4637
|
setPromptMode({ kind: "text" });
|
|
4216
|
-
promptResolveRef.current =
|
|
4638
|
+
promptResolveRef.current = resolve11;
|
|
4217
4639
|
setState(APP_STATE.PROMPTING);
|
|
4218
4640
|
controller.signal.addEventListener(
|
|
4219
4641
|
"abort",
|
|
@@ -4223,7 +4645,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
|
|
|
4223
4645
|
});
|
|
4224
4646
|
},
|
|
4225
4647
|
select(message, options) {
|
|
4226
|
-
return new Promise((
|
|
4648
|
+
return new Promise((resolve11, reject) => {
|
|
4227
4649
|
if (controller.signal.aborted) {
|
|
4228
4650
|
reject(new DOMException("Aborted", "AbortError"));
|
|
4229
4651
|
return;
|
|
@@ -4231,7 +4653,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
|
|
|
4231
4653
|
setPromptMessage(message);
|
|
4232
4654
|
setPromptMode({ kind: "select", options });
|
|
4233
4655
|
setSelectIndex(0);
|
|
4234
|
-
promptResolveRef.current =
|
|
4656
|
+
promptResolveRef.current = resolve11;
|
|
4235
4657
|
setState(APP_STATE.PROMPTING);
|
|
4236
4658
|
controller.signal.addEventListener(
|
|
4237
4659
|
"abort",
|
|
@@ -4241,7 +4663,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
|
|
|
4241
4663
|
});
|
|
4242
4664
|
},
|
|
4243
4665
|
multiselect(message, options) {
|
|
4244
|
-
return new Promise((
|
|
4666
|
+
return new Promise((resolve11, reject) => {
|
|
4245
4667
|
if (controller.signal.aborted) {
|
|
4246
4668
|
reject(new DOMException("Aborted", "AbortError"));
|
|
4247
4669
|
return;
|
|
@@ -4251,7 +4673,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
|
|
|
4251
4673
|
setSelectIndex(0);
|
|
4252
4674
|
setMultiselectChecked(new Set(options.map((_, i) => i)));
|
|
4253
4675
|
promptResolveRef.current = (value) => {
|
|
4254
|
-
|
|
4676
|
+
resolve11(value ? value.split(",") : []);
|
|
4255
4677
|
};
|
|
4256
4678
|
setState(APP_STATE.PROMPTING);
|
|
4257
4679
|
controller.signal.addEventListener(
|
|
@@ -4262,7 +4684,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
|
|
|
4262
4684
|
});
|
|
4263
4685
|
},
|
|
4264
4686
|
confirm(message, defaultValue) {
|
|
4265
|
-
return new Promise((
|
|
4687
|
+
return new Promise((resolve11, reject) => {
|
|
4266
4688
|
if (controller.signal.aborted) {
|
|
4267
4689
|
reject(new DOMException("Aborted", "AbortError"));
|
|
4268
4690
|
return;
|
|
@@ -4270,7 +4692,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
|
|
|
4270
4692
|
setPromptMessage(message);
|
|
4271
4693
|
setPromptMode({ kind: "confirm" });
|
|
4272
4694
|
setConfirmValue(defaultValue ?? true);
|
|
4273
|
-
promptResolveRef.current = (value) =>
|
|
4695
|
+
promptResolveRef.current = (value) => resolve11(value === "true");
|
|
4274
4696
|
setState(APP_STATE.PROMPTING);
|
|
4275
4697
|
controller.signal.addEventListener(
|
|
4276
4698
|
"abort",
|
|
@@ -4304,11 +4726,11 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
|
|
|
4304
4726
|
);
|
|
4305
4727
|
const handlePromptSubmit = useCallback(
|
|
4306
4728
|
(value) => {
|
|
4307
|
-
const
|
|
4729
|
+
const resolve11 = promptResolveRef.current;
|
|
4308
4730
|
promptResolveRef.current = null;
|
|
4309
4731
|
setPromptMode({ kind: "text" });
|
|
4310
4732
|
setState(APP_STATE.EXECUTING);
|
|
4311
|
-
|
|
4733
|
+
resolve11?.(value);
|
|
4312
4734
|
},
|
|
4313
4735
|
[setState]
|
|
4314
4736
|
);
|
|
@@ -4904,7 +5326,7 @@ async function standaloneConfigShowCliController(args2) {
|
|
|
4904
5326
|
const [pn, an] = parts;
|
|
4905
5327
|
const config = await resolveAppConfig(pn, an);
|
|
4906
5328
|
if (config) {
|
|
4907
|
-
console.log(JSON.stringify(config, null, 2));
|
|
5329
|
+
console.log(JSON.stringify(maskSensitiveFields(config), null, 2));
|
|
4908
5330
|
} else {
|
|
4909
5331
|
console.error(`No config found for ${appArg}.`);
|
|
4910
5332
|
process.exit(1);
|
|
@@ -4915,12 +5337,12 @@ async function standaloneConfigShowCliController(args2) {
|
|
|
4915
5337
|
if (detected) {
|
|
4916
5338
|
const config = await resolveAppConfig(detected.platformName, detected.applicationName);
|
|
4917
5339
|
if (config) {
|
|
4918
|
-
console.log(JSON.stringify(config, null, 2));
|
|
5340
|
+
console.log(JSON.stringify(maskSensitiveFields(config), null, 2));
|
|
4919
5341
|
return;
|
|
4920
5342
|
}
|
|
4921
5343
|
}
|
|
4922
5344
|
const cache = await readCacheFile();
|
|
4923
|
-
console.log(JSON.stringify(cache, null, 2));
|
|
5345
|
+
console.log(JSON.stringify(maskSensitiveFields(cache), null, 2));
|
|
4924
5346
|
}
|
|
4925
5347
|
async function standaloneConfigSetCliController(args2) {
|
|
4926
5348
|
if (args2["defaults"] === "true") {
|
|
@@ -4994,6 +5416,102 @@ async function standaloneConfigListCliController(_args) {
|
|
|
4994
5416
|
}
|
|
4995
5417
|
}
|
|
4996
5418
|
|
|
5419
|
+
// src/controllers/cli/pipelines.cli-controller.ts
|
|
5420
|
+
async function pipelinesCliController(args2) {
|
|
5421
|
+
const {
|
|
5422
|
+
provider,
|
|
5423
|
+
targets: targetsArg,
|
|
5424
|
+
stateFile,
|
|
5425
|
+
codeFolderList,
|
|
5426
|
+
packagesFolderPattern,
|
|
5427
|
+
azureServiceConnection,
|
|
5428
|
+
acrLoginServer,
|
|
5429
|
+
nodeVersion,
|
|
5430
|
+
templatesRepoName,
|
|
5431
|
+
gitopsRepoName,
|
|
5432
|
+
services: servicesJson
|
|
5433
|
+
} = args2;
|
|
5434
|
+
if (!provider) {
|
|
5435
|
+
console.error("Error: --provider is required (github or azure)");
|
|
5436
|
+
process.exit(1);
|
|
5437
|
+
}
|
|
5438
|
+
if (provider !== "github" && provider !== "azure") {
|
|
5439
|
+
console.error('Error: --provider must be "github" or "azure"');
|
|
5440
|
+
process.exit(1);
|
|
5441
|
+
}
|
|
5442
|
+
if (provider === "azure") {
|
|
5443
|
+
if (!azureServiceConnection) {
|
|
5444
|
+
console.error("Error: --azureServiceConnection is required for Azure DevOps");
|
|
5445
|
+
process.exit(1);
|
|
5446
|
+
}
|
|
5447
|
+
if (!acrLoginServer) {
|
|
5448
|
+
console.error("Error: --acrLoginServer is required for Azure DevOps");
|
|
5449
|
+
process.exit(1);
|
|
5450
|
+
}
|
|
5451
|
+
}
|
|
5452
|
+
const layout = await findPlatformLayout();
|
|
5453
|
+
if (!layout) {
|
|
5454
|
+
console.error("Error: Cannot generate pipelines \u2014 no platform initialized in this directory.");
|
|
5455
|
+
process.exit(1);
|
|
5456
|
+
}
|
|
5457
|
+
const { rootDir, coreDirName } = layout;
|
|
5458
|
+
let manifest;
|
|
5459
|
+
try {
|
|
5460
|
+
manifest = await readManifest(rootDir, coreDirName);
|
|
5461
|
+
} catch {
|
|
5462
|
+
console.error("Error: Could not read product manifest.");
|
|
5463
|
+
process.exit(1);
|
|
5464
|
+
}
|
|
5465
|
+
const allTargets = buildTargetList(rootDir, coreDirName, manifest);
|
|
5466
|
+
let selectedTargets;
|
|
5467
|
+
if (!targetsArg || targetsArg === "all") {
|
|
5468
|
+
selectedTargets = allTargets;
|
|
5469
|
+
} else {
|
|
5470
|
+
const names = targetsArg.split(",").map((n) => n.trim());
|
|
5471
|
+
selectedTargets = allTargets.filter((t) => names.includes(t.name));
|
|
5472
|
+
if (selectedTargets.length === 0) {
|
|
5473
|
+
console.error(`Error: None of the specified targets found. Available: ${allTargets.map((t) => t.name).join(", ")}`);
|
|
5474
|
+
process.exit(1);
|
|
5475
|
+
}
|
|
5476
|
+
}
|
|
5477
|
+
let servicesMap = {};
|
|
5478
|
+
if (servicesJson) {
|
|
5479
|
+
try {
|
|
5480
|
+
servicesMap = JSON.parse(servicesJson);
|
|
5481
|
+
} catch {
|
|
5482
|
+
console.error(`Error: --services must be valid JSON map keyed by target name (e.g. '{"myapp":[{"name":"svc","folder":"services/svc"}]}')`);
|
|
5483
|
+
process.exit(1);
|
|
5484
|
+
}
|
|
5485
|
+
}
|
|
5486
|
+
const targetsWithServices = [];
|
|
5487
|
+
for (const target of selectedTargets) {
|
|
5488
|
+
let services;
|
|
5489
|
+
if (servicesMap[target.name]) {
|
|
5490
|
+
services = servicesMap[target.name];
|
|
5491
|
+
} else if (provider === "azure") {
|
|
5492
|
+
services = await detectServicesInDir(target.dir);
|
|
5493
|
+
} else {
|
|
5494
|
+
services = [];
|
|
5495
|
+
}
|
|
5496
|
+
targetsWithServices.push({ ...target, services });
|
|
5497
|
+
}
|
|
5498
|
+
await pipelinesService(
|
|
5499
|
+
{
|
|
5500
|
+
provider,
|
|
5501
|
+
targets: targetsWithServices,
|
|
5502
|
+
stateFile,
|
|
5503
|
+
codeFolderList,
|
|
5504
|
+
packagesFolderPattern,
|
|
5505
|
+
azureServiceConnection,
|
|
5506
|
+
acrLoginServer,
|
|
5507
|
+
nodeVersion,
|
|
5508
|
+
templatesRepoName,
|
|
5509
|
+
gitopsRepoName
|
|
5510
|
+
},
|
|
5511
|
+
{ log: console.log }
|
|
5512
|
+
);
|
|
5513
|
+
}
|
|
5514
|
+
|
|
4997
5515
|
// src/controllers/cli/registry.ts
|
|
4998
5516
|
var cliControllers = /* @__PURE__ */ new Map([
|
|
4999
5517
|
[CREATE_APPLICATION_COMMAND_NAME, createApplicationCliController],
|
|
@@ -5014,7 +5532,8 @@ var cliControllers = /* @__PURE__ */ new Map([
|
|
|
5014
5532
|
[STANDALONE_CONFIG_SHOW_COMMAND_NAME, standaloneConfigShowCliController],
|
|
5015
5533
|
[STANDALONE_CONFIG_SET_COMMAND_NAME, standaloneConfigSetCliController],
|
|
5016
5534
|
[STANDALONE_CONFIG_DELETE_COMMAND_NAME, standaloneConfigDeleteCliController],
|
|
5017
|
-
[STANDALONE_CONFIG_LIST_COMMAND_NAME, standaloneConfigListCliController]
|
|
5535
|
+
[STANDALONE_CONFIG_LIST_COMMAND_NAME, standaloneConfigListCliController],
|
|
5536
|
+
[PIPELINES_COMMAND_NAME, pipelinesCliController]
|
|
5018
5537
|
]);
|
|
5019
5538
|
|
|
5020
5539
|
// src/utils/parse-args.ts
|