@bluealba/platform-cli 1.2.0-alpha.3 → 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 CHANGED
@@ -1290,7 +1290,7 @@ async function configureIdp(params, logger) {
1290
1290
 
1291
1291
  // src/commands/create-service-module/create-service-module.command.ts
1292
1292
  import { join as join22, resolve as resolve3 } from "path";
1293
- import { access as access4 } from "fs/promises";
1293
+ import { access as access5 } from "fs/promises";
1294
1294
 
1295
1295
  // src/commands/create-service-module/scaffold-service-module.ts
1296
1296
  import { fileURLToPath as fileURLToPath9 } from "url";
@@ -1354,6 +1354,46 @@ async function appendServiceToDockerCompose(dockerComposePath, serviceName, plat
1354
1354
  }
1355
1355
  }
1356
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
+
1357
1397
  // src/commands/create-service-module/create-service-module.command.ts
1358
1398
  var CREATE_SERVICE_MODULE_COMMAND_NAME = "create-service-module";
1359
1399
  var createServiceModuleCommand = {
@@ -1384,7 +1424,7 @@ async function createServiceModule(params, logger) {
1384
1424
  }
1385
1425
  const applicationDir = resolve3(join22(rootDir, coreDirName), appEntry.localPath);
1386
1426
  try {
1387
- await access4(applicationDir);
1427
+ await access5(applicationDir);
1388
1428
  } catch {
1389
1429
  logger.log(`Error: The specified application "${applicationName}" does not exist in the platform.`);
1390
1430
  return;
@@ -1393,7 +1433,7 @@ async function createServiceModule(params, logger) {
1393
1433
  const fullServiceName = suffix ? `${platformName}-${serviceName}-${suffix}` : `${platformName}-${serviceName}`;
1394
1434
  const serviceDir = join22(applicationDir, "services", fullServiceName);
1395
1435
  try {
1396
- await access4(serviceDir);
1436
+ await access5(serviceDir);
1397
1437
  logger.log(`Error: A service named "${fullServiceName}" already exists in application "${applicationName}".`);
1398
1438
  return;
1399
1439
  } catch {
@@ -1418,15 +1458,17 @@ async function createServiceModule(params, logger) {
1418
1458
  const dockerComposePath = join22(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
1419
1459
  const port = await getNextAvailablePort(localDir);
1420
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);
1421
1463
  logger.log(`Done! Service module "${fullServiceName}" added to application "${applicationName}".`);
1422
1464
  }
1423
1465
 
1424
1466
  // src/commands/create-ui-module/create-ui-module.command.ts
1425
1467
  import { join as join23, resolve as resolve4 } from "path";
1426
- import { access as access5, readdir as readdir4 } from "fs/promises";
1468
+ import { access as access6, readdir as readdir4 } from "fs/promises";
1427
1469
 
1428
1470
  // src/commands/create-ui-module/append-ui-docker-compose.ts
1429
- import { readFile as readFile10, writeFile as writeFile10 } from "fs/promises";
1471
+ import { readFile as readFile11, writeFile as writeFile11 } from "fs/promises";
1430
1472
  async function appendUiToDockerCompose(dockerComposePath, platformName, applicationName, port, logger, uiNameOverride) {
1431
1473
  const uiName = uiNameOverride ?? `${platformName}-${applicationName}-ui`;
1432
1474
  const block = `
@@ -1440,8 +1482,8 @@ async function appendUiToDockerCompose(dockerComposePath, platformName, applicat
1440
1482
  - \${PWD}/:/app/out
1441
1483
  `;
1442
1484
  try {
1443
- const existing = await readFile10(dockerComposePath, "utf-8");
1444
- await writeFile10(dockerComposePath, existing + block, "utf-8");
1485
+ const existing = await readFile11(dockerComposePath, "utf-8");
1486
+ await writeFile11(dockerComposePath, existing + block, "utf-8");
1445
1487
  logger.log(`Updated docker-compose: ${dockerComposePath}`);
1446
1488
  } catch (err) {
1447
1489
  const message = err instanceof Error ? err.message : String(err);
@@ -1479,7 +1521,7 @@ async function createUiModule(params, logger) {
1479
1521
  }
1480
1522
  const applicationDir = resolve4(join23(rootDir, coreDirName), appEntry.localPath);
1481
1523
  try {
1482
- await access5(applicationDir);
1524
+ await access6(applicationDir);
1483
1525
  } catch {
1484
1526
  logger.log(`Error: The specified application "${applicationName}" does not exist in the platform.`);
1485
1527
  return;
@@ -1510,18 +1552,20 @@ async function createUiModule(params, logger) {
1510
1552
  const dockerComposePath = join23(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
1511
1553
  const port = await getNextAvailablePort(localDir);
1512
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);
1513
1557
  logger.log(`Done! UI module "${uiName}" added to application "${applicationName}".`);
1514
1558
  }
1515
1559
 
1516
1560
  // src/commands/status/status-checks.ts
1517
1561
  import { spawn as spawn3 } from "child_process";
1518
- import { access as access8 } from "fs/promises";
1562
+ import { access as access9 } from "fs/promises";
1519
1563
  import { join as join26, resolve as resolve7 } from "path";
1520
1564
  import { fetch as undiciFetch2, Agent as Agent2 } from "undici";
1521
1565
 
1522
1566
  // src/commands/local-scripts/docker-compose-orchestrator.ts
1523
1567
  import { spawn } from "child_process";
1524
- import { access as access6, readFile as readFile11, writeFile as writeFile11 } from "fs/promises";
1568
+ import { access as access7, readFile as readFile12, writeFile as writeFile12 } from "fs/promises";
1525
1569
  import { dirname as dirname12, join as join24, resolve as resolve5 } from "path";
1526
1570
  function runDockerCompose(args2, logger, rootDir, signal) {
1527
1571
  return new Promise((resolvePromise) => {
@@ -1587,7 +1631,7 @@ function captureDockerCompose(args2, rootDir) {
1587
1631
  }
1588
1632
  async function generateProxyAppCompose(appDir, localDir, platformName, applicationName) {
1589
1633
  const appComposePath = join24(appDir, "docker-compose.yml");
1590
- const content = await readFile11(appComposePath, "utf-8");
1634
+ const content = await readFile12(appComposePath, "utf-8");
1591
1635
  const withAbsContexts = content.replace(
1592
1636
  /^(\s+context:\s+)\.\/(.*)$/gm,
1593
1637
  `$1${appDir}/$2`
@@ -1595,7 +1639,7 @@ async function generateProxyAppCompose(appDir, localDir, platformName, applicati
1595
1639
  const appParentDir = dirname12(appDir);
1596
1640
  const rewritten = withAbsContexts.replace(/\$\{PWD\}/g, appParentDir);
1597
1641
  const proxyPath = join24(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
1598
- await writeFile11(proxyPath, rewritten, "utf-8");
1642
+ await writeFile12(proxyPath, rewritten, "utf-8");
1599
1643
  return proxyPath;
1600
1644
  }
1601
1645
  async function getAppComposePaths(localDir, coreDir, platformName, manifest, logger) {
@@ -1607,16 +1651,16 @@ async function getAppComposePaths(localDir, coreDir, platformName, manifest, log
1607
1651
  const legacyUnprefixedPath = join24(localDir, `${app.name}-docker-compose.yml`);
1608
1652
  let resolved = null;
1609
1653
  try {
1610
- await access6(newPath);
1654
+ await access7(newPath);
1611
1655
  resolved = await generateProxyAppCompose(appDir, localDir, platformName, app.name);
1612
1656
  } catch {
1613
1657
  try {
1614
- await access6(legacyPrefixedPath);
1658
+ await access7(legacyPrefixedPath);
1615
1659
  resolved = legacyPrefixedPath;
1616
1660
  logger?.log(`Warning: docker-compose for "${app.name}" found in legacy location (core/local/). Consider moving it to the app monorepo root.`);
1617
1661
  } catch {
1618
1662
  try {
1619
- await access6(legacyUnprefixedPath);
1663
+ await access7(legacyUnprefixedPath);
1620
1664
  resolved = legacyUnprefixedPath;
1621
1665
  logger?.log(`Warning: docker-compose for "${app.name}" found in legacy location (core/local/). Consider moving it to the app monorepo root.`);
1622
1666
  } catch {
@@ -1636,7 +1680,7 @@ async function buildFullComposeArgs(layout, manifest, logger) {
1636
1680
  const unprefixedCoreCompose = join24(localDir, "core-docker-compose.yml");
1637
1681
  let coreComposePath;
1638
1682
  try {
1639
- await access6(prefixedCoreCompose);
1683
+ await access7(prefixedCoreCompose);
1640
1684
  coreComposePath = prefixedCoreCompose;
1641
1685
  } catch {
1642
1686
  coreComposePath = unprefixedCoreCompose;
@@ -1667,7 +1711,7 @@ async function buildSelectedComposeFiles(layout, selectedManifest, includeCore)
1667
1711
  const unprefixedCoreCompose = join24(localDir, "core-docker-compose.yml");
1668
1712
  let coreComposePath;
1669
1713
  try {
1670
- await access6(prefixedCoreCompose);
1714
+ await access7(prefixedCoreCompose);
1671
1715
  coreComposePath = prefixedCoreCompose;
1672
1716
  } catch {
1673
1717
  coreComposePath = unprefixedCoreCompose;
@@ -1794,7 +1838,7 @@ async function destroyEnvironment(layout, manifest, logger, signal, includeCore
1794
1838
 
1795
1839
  // src/commands/local-scripts/npm-orchestrator.ts
1796
1840
  import { spawn as spawn2 } from "child_process";
1797
- import { access as access7 } from "fs/promises";
1841
+ import { access as access8 } from "fs/promises";
1798
1842
  import { resolve as resolve6, join as join25 } from "path";
1799
1843
  function runCommand(command, args2, workDir, logger, signal) {
1800
1844
  return new Promise((resolvePromise) => {
@@ -1842,7 +1886,7 @@ function runCommand(command, args2, workDir, logger, signal) {
1842
1886
  }
1843
1887
  async function dirExists(dirPath) {
1844
1888
  try {
1845
- await access7(dirPath);
1889
+ await access8(dirPath);
1846
1890
  return true;
1847
1891
  } catch {
1848
1892
  return false;
@@ -2015,7 +2059,7 @@ function spawnCapture(cmd, args2, cwd11) {
2015
2059
  }
2016
2060
  async function pathExists(p) {
2017
2061
  try {
2018
- await access8(p);
2062
+ await access9(p);
2019
2063
  return true;
2020
2064
  } catch {
2021
2065
  return false;
@@ -2681,6 +2725,132 @@ var standaloneConfigListCommand = {
2681
2725
  hidden: () => false
2682
2726
  };
2683
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
+
2684
2854
  // src/commands/registry.ts
2685
2855
  var CommandRegistry = class {
2686
2856
  commands = /* @__PURE__ */ new Map();
@@ -2728,6 +2898,7 @@ registry.register(standaloneConfigShowCommand);
2728
2898
  registry.register(standaloneConfigSetCommand);
2729
2899
  registry.register(standaloneConfigDeleteCommand);
2730
2900
  registry.register(standaloneConfigListCommand);
2901
+ registry.register(pipelinesCommand);
2731
2902
 
2732
2903
  // src/app-state.ts
2733
2904
  var APP_STATE = {
@@ -2790,6 +2961,145 @@ async function initService(params, logger) {
2790
2961
  await init(params, logger);
2791
2962
  }
2792
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
+
2793
3103
  // src/controllers/ui/init.ui-controller.ts
2794
3104
  async function initUiController(ctx) {
2795
3105
  if (await isPlatformInitialized()) {
@@ -2825,6 +3135,10 @@ async function initUiController(ctx) {
2825
3135
  { organizationName, platformName, platformDisplayName, bootstrapServiceSuffix, customizationUiSuffix },
2826
3136
  ctx
2827
3137
  );
3138
+ const addPipelines = await ctx.confirm("Configure CI/CD pipelines?", false);
3139
+ if (addPipelines) {
3140
+ await pipelinesUiController(ctx);
3141
+ }
2828
3142
  }
2829
3143
 
2830
3144
  // src/services/configure-idp.service.ts
@@ -3259,20 +3573,20 @@ async function installAiPluginService(params, context) {
3259
3573
 
3260
3574
  // src/commands/install-ai-plugin/providers/claude.provider.ts
3261
3575
  import { homedir as homedir3 } from "os";
3262
- import { join as join28 } from "path";
3263
- import { readFile as readFile12, writeFile as writeFile12, mkdir as mkdir4, rm } from "fs/promises";
3576
+ import { join as join32 } from "path";
3577
+ import { readFile as readFile13, writeFile as writeFile13, mkdir as mkdir4, rm } from "fs/promises";
3264
3578
  var ClaudeProvider = class {
3265
3579
  name = "claude";
3266
- baseDir = join28(homedir3(), ".claude");
3580
+ baseDir = join32(homedir3(), ".claude");
3267
3581
  getInstallPath(skillName) {
3268
- return join28(this.baseDir, "skills", skillName, "SKILL.md");
3582
+ return join32(this.baseDir, "skills", skillName, "SKILL.md");
3269
3583
  }
3270
3584
  manifestPath(pluginName) {
3271
- return join28(this.baseDir, `${pluginName}.manifest.json`);
3585
+ return join32(this.baseDir, `${pluginName}.manifest.json`);
3272
3586
  }
3273
3587
  async readManifest(pluginName) {
3274
3588
  try {
3275
- const content = await readFile12(this.manifestPath(pluginName), "utf-8");
3589
+ const content = await readFile13(this.manifestPath(pluginName), "utf-8");
3276
3590
  return JSON.parse(content);
3277
3591
  } catch {
3278
3592
  return null;
@@ -3280,7 +3594,7 @@ var ClaudeProvider = class {
3280
3594
  }
3281
3595
  async writeManifest(manifest) {
3282
3596
  await mkdir4(this.baseDir, { recursive: true });
3283
- await writeFile12(
3597
+ await writeFile13(
3284
3598
  this.manifestPath(manifest.plugin),
3285
3599
  JSON.stringify(manifest, null, 2),
3286
3600
  "utf-8"
@@ -3300,16 +3614,16 @@ var ClaudeProvider = class {
3300
3614
  }
3301
3615
  async installSkill(skill, docsSource, logger) {
3302
3616
  const sourcePath = docsSource.resolve(skill.sourceFile);
3303
- let content = await readFile12(sourcePath, "utf-8");
3617
+ let content = await readFile13(sourcePath, "utf-8");
3304
3618
  const docsPath = docsSource.resolve("docs");
3305
3619
  content = content.replaceAll("{{docsPath}}", docsPath);
3306
3620
  const installPath = this.getInstallPath(skill.name);
3307
- await mkdir4(join28(installPath, ".."), { recursive: true });
3308
- await writeFile12(installPath, content, "utf-8");
3621
+ await mkdir4(join32(installPath, ".."), { recursive: true });
3622
+ await writeFile13(installPath, content, "utf-8");
3309
3623
  logger.log(` Installed skill: ${installPath}`);
3310
3624
  }
3311
3625
  async removeSkill(skillName, logger) {
3312
- const skillDir = join28(this.baseDir, "skills", skillName);
3626
+ const skillDir = join32(this.baseDir, "skills", skillName);
3313
3627
  try {
3314
3628
  await rm(skillDir, { recursive: true, force: true });
3315
3629
  logger.log(` Removed skill: ${skillDir}`);
@@ -3317,11 +3631,11 @@ var ClaudeProvider = class {
3317
3631
  }
3318
3632
  }
3319
3633
  async updatePermissions(docsSource, logger) {
3320
- const settingsPath = join28(this.baseDir, "settings.json");
3634
+ const settingsPath = join32(this.baseDir, "settings.json");
3321
3635
  const docsPath = docsSource.resolve("docs");
3322
3636
  let settings = {};
3323
3637
  try {
3324
- const content = await readFile12(settingsPath, "utf-8");
3638
+ const content = await readFile13(settingsPath, "utf-8");
3325
3639
  settings = JSON.parse(content);
3326
3640
  } catch {
3327
3641
  }
@@ -3333,7 +3647,7 @@ var ClaudeProvider = class {
3333
3647
  permissions.additionalDirectories = [...additionalDirectories, docsPath];
3334
3648
  settings.permissions = permissions;
3335
3649
  await mkdir4(this.baseDir, { recursive: true });
3336
- await writeFile12(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
3650
+ await writeFile13(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
3337
3651
  logger.log(` Granted read access to docs: ${docsPath}`);
3338
3652
  }
3339
3653
  };
@@ -3352,12 +3666,12 @@ function getProvider(name) {
3352
3666
  }
3353
3667
 
3354
3668
  // src/commands/install-ai-plugin/docs-source/local.docs-source.ts
3355
- import { fileURLToPath as fileURLToPath10 } from "url";
3356
- import { join as join29, dirname as dirname13 } from "path";
3357
- var packageRoot = join29(dirname13(fileURLToPath10(import.meta.url)), "..");
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)), "..");
3358
3672
  var LocalDocsSource = class {
3359
3673
  resolve(relativePath) {
3360
- return join29(packageRoot, relativePath);
3674
+ return join33(packageRoot, relativePath);
3361
3675
  }
3362
3676
  };
3363
3677
 
@@ -3401,20 +3715,20 @@ async function installAiPluginUiController(ctx) {
3401
3715
 
3402
3716
  // src/controllers/ui/standalone.ui-controller.ts
3403
3717
  import { rm as rm3 } from "fs/promises";
3404
- import { join as join33 } from "path";
3718
+ import { join as join37 } from "path";
3405
3719
  import { cwd as cwd7 } from "process";
3406
3720
  import { exec } from "child_process";
3407
3721
 
3408
3722
  // src/commands/standalone/app-monorepo-check.ts
3409
- import { access as access9, readdir as readdir5 } from "fs/promises";
3410
- import { join as join30 } from "path";
3723
+ import { access as access11, readdir as readdir6 } from "fs/promises";
3724
+ import { join as join34 } from "path";
3411
3725
  import { cwd as cwd6 } from "process";
3412
3726
  async function findAppMonorepoLayout(startDir = cwd6()) {
3413
- const composePath = join30(startDir, "docker-compose.yml");
3414
- const packageJsonPath = join30(startDir, "package.json");
3727
+ const composePath = join34(startDir, "docker-compose.yml");
3728
+ const packageJsonPath = join34(startDir, "package.json");
3415
3729
  try {
3416
- await access9(composePath);
3417
- await access9(packageJsonPath);
3730
+ await access11(composePath);
3731
+ await access11(packageJsonPath);
3418
3732
  } catch {
3419
3733
  return null;
3420
3734
  }
@@ -3425,11 +3739,11 @@ async function findAppMonorepoLayout(startDir = cwd6()) {
3425
3739
  }
3426
3740
  async function hasCoreChildDir(dir) {
3427
3741
  try {
3428
- const entries = await readdir5(dir, { withFileTypes: true });
3742
+ const entries = await readdir6(dir, { withFileTypes: true });
3429
3743
  for (const entry of entries) {
3430
3744
  if (entry.isDirectory() && (entry.name.endsWith("-core") || entry.name === "core")) {
3431
3745
  try {
3432
- await access9(join30(dir, entry.name, "product.manifest.json"));
3746
+ await access11(join34(dir, entry.name, "product.manifest.json"));
3433
3747
  return true;
3434
3748
  } catch {
3435
3749
  }
@@ -3444,36 +3758,36 @@ async function isInAppMonorepo() {
3444
3758
  }
3445
3759
 
3446
3760
  // src/commands/standalone/standalone-auto-detect.ts
3447
- import { readdir as readdir6, readFile as readFile13 } from "fs/promises";
3448
- import { join as join31, basename as basename2 } from "path";
3761
+ import { readdir as readdir7, readFile as readFile14 } from "fs/promises";
3762
+ import { join as join35, basename as basename2 } from "path";
3449
3763
  async function autoDetectAppIdentity(appDir) {
3450
3764
  const fromBootstrap = await detectFromBootstrap(appDir);
3451
3765
  if (fromBootstrap) return fromBootstrap;
3452
3766
  return detectFromPackageJson(appDir);
3453
3767
  }
3454
3768
  async function detectFromBootstrap(appDir) {
3455
- const servicesDir = join31(appDir, "services");
3769
+ const servicesDir = join35(appDir, "services");
3456
3770
  let entries;
3457
3771
  try {
3458
- entries = await readdir6(servicesDir);
3772
+ entries = await readdir7(servicesDir);
3459
3773
  } catch {
3460
3774
  return null;
3461
3775
  }
3462
3776
  const bootstrapDirs = entries.filter((e) => e.endsWith("-bootstrap-service"));
3463
3777
  if (bootstrapDirs.length === 0) return null;
3464
3778
  for (const bootstrapDirName of bootstrapDirs) {
3465
- const dataDir = join31(servicesDir, bootstrapDirName, "src", "data");
3779
+ const dataDir = join35(servicesDir, bootstrapDirName, "src", "data");
3466
3780
  let dataDirEntries;
3467
3781
  try {
3468
- dataDirEntries = await readdir6(dataDir);
3782
+ dataDirEntries = await readdir7(dataDir);
3469
3783
  } catch {
3470
3784
  continue;
3471
3785
  }
3472
3786
  for (const subDir of dataDirEntries) {
3473
3787
  if (subDir === "platform") continue;
3474
- const appJsonPath = join31(dataDir, subDir, "application.json");
3788
+ const appJsonPath = join35(dataDir, subDir, "application.json");
3475
3789
  try {
3476
- const content = await readFile13(appJsonPath, "utf-8");
3790
+ const content = await readFile14(appJsonPath, "utf-8");
3477
3791
  const appData = JSON.parse(content);
3478
3792
  if (!appData.name) continue;
3479
3793
  const applicationName = appData.name;
@@ -3491,7 +3805,7 @@ async function detectFromBootstrap(appDir) {
3491
3805
  }
3492
3806
  async function detectFromPackageJson(appDir) {
3493
3807
  try {
3494
- const content = await readFile13(join31(appDir, "package.json"), "utf-8");
3808
+ const content = await readFile14(join35(appDir, "package.json"), "utf-8");
3495
3809
  const pkg = JSON.parse(content);
3496
3810
  const rawName = pkg.name ?? "";
3497
3811
  const applicationName = rawName.includes("/") ? rawName.split("/")[1] ?? rawName : rawName;
@@ -3514,24 +3828,24 @@ function inferPlatformNameFromAppDir(appDir, applicationName) {
3514
3828
  }
3515
3829
 
3516
3830
  // src/commands/standalone/standalone-orchestrator.ts
3517
- import { join as join32, dirname as dirname14 } from "path";
3518
- import { mkdir as mkdir5, rm as rm2, readdir as readdir7, access as access10, writeFile as writeFile13, readFile as readFile14, open } from "fs/promises";
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";
3519
3833
  import { constants } from "fs";
3520
3834
  import { fetch as undiciFetch4, Agent as Agent4 } from "undici";
3521
3835
  var TMP_BASE = "/tmp";
3522
3836
  var STANDALONE_PREFIX = "platform-standalone-";
3523
3837
  var DETACH_MARKER = ".standalone-running";
3524
3838
  function getTmpDir(platformName) {
3525
- return join32(TMP_BASE, `${STANDALONE_PREFIX}${platformName}`);
3839
+ return join36(TMP_BASE, `${STANDALONE_PREFIX}${platformName}`);
3526
3840
  }
3527
3841
  async function findRunningSingleton() {
3528
3842
  try {
3529
- const entries = await readdir7(TMP_BASE, { withFileTypes: true });
3843
+ const entries = await readdir8(TMP_BASE, { withFileTypes: true });
3530
3844
  for (const entry of entries) {
3531
3845
  if (entry.isDirectory() && entry.name.startsWith(STANDALONE_PREFIX)) {
3532
- const candidateDir = join32(TMP_BASE, entry.name);
3846
+ const candidateDir = join36(TMP_BASE, entry.name);
3533
3847
  try {
3534
- await access10(join32(candidateDir, DETACH_MARKER));
3848
+ await access12(join36(candidateDir, DETACH_MARKER));
3535
3849
  return candidateDir;
3536
3850
  } catch {
3537
3851
  }
@@ -3575,7 +3889,7 @@ async function configureIdpFromConfig(config, localDir, logger) {
3575
3889
  logger.log("No IDP configured \u2014 run 'platform standalone-config set' to add an IDP. Skipping IDP setup.");
3576
3890
  return;
3577
3891
  }
3578
- const envPath = join32(localDir, ".env");
3892
+ const envPath = join36(localDir, ".env");
3579
3893
  let env;
3580
3894
  try {
3581
3895
  env = await readEnvFile(envPath);
@@ -3630,7 +3944,7 @@ async function configureAdminUsersFromConfig(config, localDir, logger) {
3630
3944
  if (!config.adminUsers || config.adminUsers.length === 0) {
3631
3945
  return;
3632
3946
  }
3633
- const envPath = join32(localDir, ".env");
3947
+ const envPath = join36(localDir, ".env");
3634
3948
  let env;
3635
3949
  try {
3636
3950
  env = await readEnvFile(envPath);
@@ -3673,19 +3987,19 @@ async function configureAdminUsersFromConfig(config, localDir, logger) {
3673
3987
  }
3674
3988
  }
3675
3989
  function buildLayout(tmpDir, coreDirName) {
3676
- const coreDir = join32(tmpDir, coreDirName);
3990
+ const coreDir = join36(tmpDir, coreDirName);
3677
3991
  return {
3678
3992
  rootDir: tmpDir,
3679
3993
  coreDir,
3680
3994
  coreDirName,
3681
- localDir: join32(coreDir, "local")
3995
+ localDir: join36(coreDir, "local")
3682
3996
  };
3683
3997
  }
3684
3998
  async function generateProxyAppCompose2(appDir, localDir, platformName, applicationName, logger) {
3685
- const appComposePath = join32(appDir, "docker-compose.yml");
3999
+ const appComposePath = join36(appDir, "docker-compose.yml");
3686
4000
  let content;
3687
4001
  try {
3688
- content = await readFile14(appComposePath, "utf-8");
4002
+ content = await readFile15(appComposePath, "utf-8");
3689
4003
  } catch {
3690
4004
  logger.log(`Warning: No docker-compose.yml found at ${appComposePath} \u2014 app services won't start.`);
3691
4005
  return;
@@ -3694,10 +4008,10 @@ async function generateProxyAppCompose2(appDir, localDir, platformName, applicat
3694
4008
  /^(\s+context:\s+)\.\/(.*)$/gm,
3695
4009
  `$1${appDir}/$2`
3696
4010
  );
3697
- const appParentDir = dirname14(appDir);
4011
+ const appParentDir = dirname16(appDir);
3698
4012
  const rewritten = withAbsContexts.replace(/\$\{PWD\}/g, appParentDir);
3699
- const proxyPath = join32(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
3700
- await writeFile13(proxyPath, rewritten, "utf-8");
4013
+ const proxyPath = join36(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
4014
+ await writeFile14(proxyPath, rewritten, "utf-8");
3701
4015
  logger.log(`Generated app compose with absolute build paths.`);
3702
4016
  }
3703
4017
  function buildComposeManifest(manifest) {
@@ -3726,7 +4040,7 @@ async function startStandalone(config, appDir, logger, signal, detach = false) {
3726
4040
  let markerFd;
3727
4041
  try {
3728
4042
  markerFd = await open(
3729
- join32(tmpDir, DETACH_MARKER),
4043
+ join36(tmpDir, DETACH_MARKER),
3730
4044
  constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY
3731
4045
  );
3732
4046
  await markerFd.writeFile(platformName, "utf-8");
@@ -3764,9 +4078,9 @@ async function startStandalone(config, appDir, logger, signal, detach = false) {
3764
4078
  }
3765
4079
  );
3766
4080
  await writeManifest(npmManifest, tmpDir, coreDirName);
3767
- const bootstrapServiceDir = join32(tmpDir, coreDirName, "services", bootstrapServiceName);
4081
+ const bootstrapServiceDir = join36(tmpDir, coreDirName, "services", bootstrapServiceName);
3768
4082
  await scaffoldPlatformBootstrap(bootstrapServiceDir, variables, logger);
3769
- const customizationUiDir = join32(tmpDir, coreDirName, "ui", customizationUiName);
4083
+ const customizationUiDir = join36(tmpDir, coreDirName, "ui", customizationUiName);
3770
4084
  await scaffoldCustomizationUi(customizationUiDir, variables, logger);
3771
4085
  await registerCustomizationModule(bootstrapServiceDir, organizationName, logger, platformName);
3772
4086
  const layout = buildLayout(tmpDir, coreDirName);
@@ -3785,7 +4099,7 @@ async function startStandalone(config, appDir, logger, signal, detach = false) {
3785
4099
  logger.log("Starting containers...");
3786
4100
  await startEnvironment(layout, composeManifest, logger, signal);
3787
4101
  if (signal?.aborted) return;
3788
- const env = await readEnvFile(join32(layout.localDir, ".env")).catch(() => /* @__PURE__ */ new Map());
4102
+ const env = await readEnvFile(join36(layout.localDir, ".env")).catch(() => /* @__PURE__ */ new Map());
3789
4103
  const gatewayUrl = env.get("PAE_GATEWAY_HOST_URL") ?? "https://localhost:443";
3790
4104
  const gatewayReady = await waitForGateway(gatewayUrl, logger);
3791
4105
  if (!gatewayReady) {
@@ -3835,11 +4149,11 @@ async function stopStandalone(logger) {
3835
4149
  }
3836
4150
  let coreDirName = null;
3837
4151
  try {
3838
- const entries = await readdir7(runningDir, { withFileTypes: true });
4152
+ const entries = await readdir8(runningDir, { withFileTypes: true });
3839
4153
  for (const entry of entries) {
3840
4154
  if (entry.isDirectory() && entry.name.endsWith("-core")) {
3841
4155
  try {
3842
- await access10(join32(runningDir, entry.name, "product.manifest.json"));
4156
+ await access12(join36(runningDir, entry.name, "product.manifest.json"));
3843
4157
  coreDirName = entry.name;
3844
4158
  break;
3845
4159
  } catch {
@@ -3876,9 +4190,9 @@ async function stopStandalone(logger) {
3876
4190
 
3877
4191
  // src/controllers/ui/standalone.ui-controller.ts
3878
4192
  function getGitEmail() {
3879
- return new Promise((resolve9) => {
4193
+ return new Promise((resolve11) => {
3880
4194
  exec("git config user.email", (err, stdout) => {
3881
- resolve9(err ? "" : stdout.trim());
4195
+ resolve11(err ? "" : stdout.trim());
3882
4196
  });
3883
4197
  });
3884
4198
  }
@@ -3954,7 +4268,7 @@ async function standaloneUiController(ctx) {
3954
4268
  const deleteLegacy = await ctx.confirm("Delete the local standalone.json?", true);
3955
4269
  if (deleteLegacy) {
3956
4270
  try {
3957
- await rm3(join33(appDir, "standalone.json"));
4271
+ await rm3(join37(appDir, "standalone.json"));
3958
4272
  ctx.log("Deleted standalone.json.");
3959
4273
  } catch {
3960
4274
  ctx.log("Warning: Could not delete standalone.json.");
@@ -4237,9 +4551,9 @@ async function collectIdpConfig2(ctx, current) {
4237
4551
  return { provider: providerType, name, issuer, clientId, clientSecret, extras };
4238
4552
  }
4239
4553
  function getGitEmail2() {
4240
- return new Promise((resolve9) => {
4554
+ return new Promise((resolve11) => {
4241
4555
  exec2("git config user.email", (err, stdout) => {
4242
- resolve9(err ? "" : stdout.trim());
4556
+ resolve11(err ? "" : stdout.trim());
4243
4557
  });
4244
4558
  });
4245
4559
  }
@@ -4278,7 +4592,8 @@ var uiControllers = /* @__PURE__ */ new Map([
4278
4592
  [STANDALONE_CONFIG_SHOW_COMMAND_NAME, standaloneConfigShowUiController],
4279
4593
  [STANDALONE_CONFIG_SET_COMMAND_NAME, standaloneConfigSetUiController],
4280
4594
  [STANDALONE_CONFIG_DELETE_COMMAND_NAME, standaloneConfigDeleteUiController],
4281
- [STANDALONE_CONFIG_LIST_COMMAND_NAME, standaloneConfigListUiController]
4595
+ [STANDALONE_CONFIG_LIST_COMMAND_NAME, standaloneConfigListUiController],
4596
+ [PIPELINES_COMMAND_NAME, pipelinesUiController]
4282
4597
  ]);
4283
4598
 
4284
4599
  // src/hooks/use-command-runner.ts
@@ -4312,7 +4627,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
4312
4627
  }
4313
4628
  },
4314
4629
  prompt(message, defaultValue) {
4315
- return new Promise((resolve9, reject) => {
4630
+ return new Promise((resolve11, reject) => {
4316
4631
  if (controller.signal.aborted) {
4317
4632
  reject(new DOMException("Aborted", "AbortError"));
4318
4633
  return;
@@ -4320,7 +4635,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
4320
4635
  setPromptMessage(message);
4321
4636
  setPromptValue(defaultValue ?? "");
4322
4637
  setPromptMode({ kind: "text" });
4323
- promptResolveRef.current = resolve9;
4638
+ promptResolveRef.current = resolve11;
4324
4639
  setState(APP_STATE.PROMPTING);
4325
4640
  controller.signal.addEventListener(
4326
4641
  "abort",
@@ -4330,7 +4645,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
4330
4645
  });
4331
4646
  },
4332
4647
  select(message, options) {
4333
- return new Promise((resolve9, reject) => {
4648
+ return new Promise((resolve11, reject) => {
4334
4649
  if (controller.signal.aborted) {
4335
4650
  reject(new DOMException("Aborted", "AbortError"));
4336
4651
  return;
@@ -4338,7 +4653,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
4338
4653
  setPromptMessage(message);
4339
4654
  setPromptMode({ kind: "select", options });
4340
4655
  setSelectIndex(0);
4341
- promptResolveRef.current = resolve9;
4656
+ promptResolveRef.current = resolve11;
4342
4657
  setState(APP_STATE.PROMPTING);
4343
4658
  controller.signal.addEventListener(
4344
4659
  "abort",
@@ -4348,7 +4663,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
4348
4663
  });
4349
4664
  },
4350
4665
  multiselect(message, options) {
4351
- return new Promise((resolve9, reject) => {
4666
+ return new Promise((resolve11, reject) => {
4352
4667
  if (controller.signal.aborted) {
4353
4668
  reject(new DOMException("Aborted", "AbortError"));
4354
4669
  return;
@@ -4358,7 +4673,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
4358
4673
  setSelectIndex(0);
4359
4674
  setMultiselectChecked(new Set(options.map((_, i) => i)));
4360
4675
  promptResolveRef.current = (value) => {
4361
- resolve9(value ? value.split(",") : []);
4676
+ resolve11(value ? value.split(",") : []);
4362
4677
  };
4363
4678
  setState(APP_STATE.PROMPTING);
4364
4679
  controller.signal.addEventListener(
@@ -4369,7 +4684,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
4369
4684
  });
4370
4685
  },
4371
4686
  confirm(message, defaultValue) {
4372
- return new Promise((resolve9, reject) => {
4687
+ return new Promise((resolve11, reject) => {
4373
4688
  if (controller.signal.aborted) {
4374
4689
  reject(new DOMException("Aborted", "AbortError"));
4375
4690
  return;
@@ -4377,7 +4692,7 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
4377
4692
  setPromptMessage(message);
4378
4693
  setPromptMode({ kind: "confirm" });
4379
4694
  setConfirmValue(defaultValue ?? true);
4380
- promptResolveRef.current = (value) => resolve9(value === "true");
4695
+ promptResolveRef.current = (value) => resolve11(value === "true");
4381
4696
  setState(APP_STATE.PROMPTING);
4382
4697
  controller.signal.addEventListener(
4383
4698
  "abort",
@@ -4411,11 +4726,11 @@ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
4411
4726
  );
4412
4727
  const handlePromptSubmit = useCallback(
4413
4728
  (value) => {
4414
- const resolve9 = promptResolveRef.current;
4729
+ const resolve11 = promptResolveRef.current;
4415
4730
  promptResolveRef.current = null;
4416
4731
  setPromptMode({ kind: "text" });
4417
4732
  setState(APP_STATE.EXECUTING);
4418
- resolve9?.(value);
4733
+ resolve11?.(value);
4419
4734
  },
4420
4735
  [setState]
4421
4736
  );
@@ -5101,6 +5416,102 @@ async function standaloneConfigListCliController(_args) {
5101
5416
  }
5102
5417
  }
5103
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
+
5104
5515
  // src/controllers/cli/registry.ts
5105
5516
  var cliControllers = /* @__PURE__ */ new Map([
5106
5517
  [CREATE_APPLICATION_COMMAND_NAME, createApplicationCliController],
@@ -5121,7 +5532,8 @@ var cliControllers = /* @__PURE__ */ new Map([
5121
5532
  [STANDALONE_CONFIG_SHOW_COMMAND_NAME, standaloneConfigShowCliController],
5122
5533
  [STANDALONE_CONFIG_SET_COMMAND_NAME, standaloneConfigSetCliController],
5123
5534
  [STANDALONE_CONFIG_DELETE_COMMAND_NAME, standaloneConfigDeleteCliController],
5124
- [STANDALONE_CONFIG_LIST_COMMAND_NAME, standaloneConfigListCliController]
5535
+ [STANDALONE_CONFIG_LIST_COMMAND_NAME, standaloneConfigListCliController],
5536
+ [PIPELINES_COMMAND_NAME, pipelinesCliController]
5125
5537
  ]);
5126
5538
 
5127
5539
  // src/utils/parse-args.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bluealba/platform-cli",
3
- "version": "1.2.0-alpha.3",
3
+ "version": "1.2.0-alpha.4",
4
4
  "description": "Blue Alba Platform CLI",
5
5
  "license": "PolyForm-Noncommercial-1.0.0",
6
6
  "type": "module",
@@ -0,0 +1,52 @@
1
+ trigger:
2
+ branches:
3
+ include:
4
+ - develop
5
+ - release/*
6
+ - rc/*
7
+ - hotfix/*
8
+ - feature/*
9
+ paths:
10
+ exclude:
11
+ - '**/*.md'
12
+ - docs/**
13
+
14
+ pr:
15
+ branches:
16
+ include:
17
+ - develop
18
+ paths:
19
+ exclude:
20
+ - '**/*.md'
21
+ - docs/**
22
+
23
+ name: '$(Date:yyyyMMdd).$(Rev:r)'
24
+
25
+ resources:
26
+ repositories:
27
+ - repository: templates
28
+ type: git
29
+ name: {{templatesRepoName}}
30
+ ref: refs/heads/main
31
+ - repository: gitops
32
+ type: git
33
+ name: {{gitopsRepoName}}
34
+
35
+ stages:
36
+
37
+ - template: azure-pipeline-monorepo-feature-build.yml@templates
38
+ parameters:
39
+ nodeVersion: '{{nodeVersion}}'
40
+ npmScopeRegistries: ''
41
+ npmAuthServiceConnections: ''
42
+
43
+ - template: azure-pipeline-monorepo-build.yml@templates
44
+ parameters:
45
+ azureServiceConnection: '{{azureServiceConnection}}'
46
+ acrLoginServer: '{{acrLoginServer}}'
47
+ nodeVersion: '{{nodeVersion}}'
48
+ npmScopeRegistries: ''
49
+ npmAuthServiceConnections: ''
50
+ services:
51
+ # platform-cli:managed-services-start
52
+ {{servicesList}} # platform-cli:managed-services-end
@@ -0,0 +1,24 @@
1
+ name: Build Platform
2
+ on:
3
+ push:
4
+ paths:
5
+ - "apps/**"
6
+ - "services/**"
7
+ - "packages/**"
8
+ - "package-lock.json"
9
+ - ".github/workflows/**"
10
+ branches-ignore:
11
+ - "dependabot/**"
12
+ workflow_dispatch:
13
+
14
+ jobs:
15
+ build_platform:
16
+ name: Build, test and publish container images for all monorepo services
17
+ uses: Blue-Alba-Platform/workflows/.github/workflows/monorepo-turbo-npm-build.yml@main
18
+ with:
19
+ code_folder_list: {{codeFolderList}}
20
+ packages_folder: "{{packagesFolderPattern}}"
21
+ full_rebuild: ${{ contains(github.ref , 'refs/heads/develop') || contains(github.ref , 'refs/heads/feature/') }}
22
+ publish: ${{ ! contains(github.ref , 'refs/heads/feature') }}
23
+ state_file: {{stateFile}}
24
+ secrets: inherit