@bluealba/platform-cli 0.3.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (20) hide show
  1. package/dist/index.js +2068 -457
  2. package/package.json +7 -6
  3. package/templates/bootstrap-service-template/Dockerfile +1 -1
  4. package/templates/bootstrap-service-template/Dockerfile.development +1 -1
  5. package/templates/bootstrap-service-template/README.md +1 -1
  6. package/templates/bootstrap-service-template/package.json +5 -5
  7. package/templates/customization-ui-module-template/package.json +3 -3
  8. package/templates/platform-init-template/{{{platformName}}-local → {{platformName}}-core/local}/platform-docker-compose.yml +6 -6
  9. package/templates/platform-init-template/{{{platformName}}-local → {{platformName}}-core/local}/{{platformName}}-core-docker-compose.yml +7 -7
  10. package/templates/react-ui-module-template/package.json +2 -2
  11. package/templates/react-ui-module-template/src/Icon.tsx +1 -1
  12. package/templates/platform-init-template/{{platformName}}-local/docker-compose.yml +0 -3
  13. package/templates/platform-init-template/{{platformName}}-local/package.json +0 -18
  14. package/templates/platform-init-template/{{platformName}}-local/scripts/build.sh +0 -18
  15. package/templates/platform-init-template/{{platformName}}-local/scripts/install.sh +0 -18
  16. /package/templates/platform-init-template/{{{platformName}}-local → {{platformName}}-core/local}/.env.example +0 -0
  17. /package/templates/platform-init-template/{{{platformName}}-local → {{platformName}}-core/local}/environment/pae-nestjs-gateway-service.env +0 -0
  18. /package/templates/platform-init-template/{{{platformName}}-local → {{platformName}}-core/local}/nginx.conf +0 -0
  19. /package/templates/platform-init-template/{{{platformName}}-local → {{platformName}}-core/local}/ssl/cert.pem +0 -0
  20. /package/templates/platform-init-template/{{{platformName}}-local → {{platformName}}-core/local}/ssl/key.pem +0 -0
package/dist/index.js CHANGED
@@ -5,8 +5,8 @@ import { render } from "ink";
5
5
 
6
6
  // src/app.tsx
7
7
  import { createRequire } from "module";
8
- import { useState as useState3, useCallback as useCallback2 } from "react";
9
- import { Box as Box5, Text as Text7, useApp, useInput } from "ink";
8
+ import { useState as useState3, useCallback as useCallback2, useEffect as useEffect2 } from "react";
9
+ import { Box as Box7, Text as Text10, useApp, useInput } from "ink";
10
10
 
11
11
  // src/components/prompt.tsx
12
12
  import { Box, Text } from "ink";
@@ -58,20 +58,25 @@ import { Static, Text as Text5 } from "ink";
58
58
 
59
59
  // src/components/welcome-banner.tsx
60
60
  import React3 from "react";
61
- import { Box as Box4, Text as Text4 } from "ink";
61
+ import { Box as Box3, Text as Text4 } from "ink";
62
62
 
63
63
  // src/components/working-directory.tsx
64
64
  import React2 from "react";
65
- import os from "os";
66
- import { Box as Box3, Text as Text3 } from "ink";
65
+ import { Text as Text3 } from "ink";
66
+ import { cwd } from "process";
67
+ import { homedir } from "os";
67
68
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
69
+ function shortenPath(fullPath) {
70
+ const home = homedir();
71
+ return fullPath.startsWith(home) ? `~${fullPath.slice(home.length)}` : fullPath;
72
+ }
68
73
  var WorkingDirectory = React2.memo(function WorkingDirectory2() {
69
- const cwd7 = process.cwd();
70
- const home = os.homedir();
71
- const displayPath = cwd7.startsWith(home) ? "~" + cwd7.slice(home.length) : cwd7;
72
- return /* @__PURE__ */ jsxs3(Box3, { paddingLeft: 2, children: [
73
- /* @__PURE__ */ jsx3(Text3, { bold: true, dimColor: true, children: "Working Dir: " }),
74
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: displayPath })
74
+ const dir = shortenPath(cwd());
75
+ return /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
76
+ " ",
77
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "cwd:" }),
78
+ " ",
79
+ dir
75
80
  ] });
76
81
  });
77
82
 
@@ -84,19 +89,19 @@ var ASCII_ART = [
84
89
  "|---' '---|"
85
90
  ];
86
91
  var WelcomeBanner = React3.memo(function WelcomeBanner2({ version: version2 }) {
87
- return /* @__PURE__ */ jsxs4(Box4, { borderStyle: "round", flexDirection: "column", paddingX: 1, paddingY: 1, children: [
92
+ return /* @__PURE__ */ jsxs4(Box3, { borderStyle: "round", flexDirection: "column", paddingX: 1, paddingY: 1, children: [
88
93
  /* @__PURE__ */ jsxs4(Text4, { bold: true, color: "cyan", children: [
89
94
  "Blue Alba Platform CLI v",
90
95
  version2
91
96
  ] }),
92
- /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
93
- /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", marginRight: 2, children: ASCII_ART.map((line, i) => /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: line }, i)) }),
94
- /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", justifyContent: "center", marginRight: 2, children: [
97
+ /* @__PURE__ */ jsxs4(Box3, { marginTop: 1, children: [
98
+ /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", marginRight: 2, children: ASCII_ART.map((line, i) => /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: line }, i)) }),
99
+ /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", justifyContent: "center", marginRight: 2, children: [
95
100
  /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Welcome to the" }),
96
101
  /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Blue Alba Platform!" })
97
102
  ] }),
98
- /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: ASCII_ART.map((_, i) => /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2502" }, i)) }),
99
- /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginLeft: 2, children: [
103
+ /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", children: ASCII_ART.map((_, i) => /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2502" }, i)) }),
104
+ /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", marginLeft: 2, children: [
100
105
  /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "Tips for getting started" }),
101
106
  /* @__PURE__ */ jsxs4(Text4, { children: [
102
107
  "Run ",
@@ -105,7 +110,7 @@ var WelcomeBanner = React3.memo(function WelcomeBanner2({ version: version2 }) {
105
110
  ] })
106
111
  ] })
107
112
  ] }),
108
- /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(WorkingDirectory, {}) })
113
+ /* @__PURE__ */ jsx4(WorkingDirectory, {})
109
114
  ] });
110
115
  });
111
116
 
@@ -138,12 +143,68 @@ function Spinner({ label }) {
138
143
  ] });
139
144
  }
140
145
 
146
+ // src/components/select-list.tsx
147
+ import React4 from "react";
148
+ import { Box as Box4, Text as Text7 } from "ink";
149
+ import { jsx as jsx7 } from "react/jsx-runtime";
150
+ var SelectList = React4.memo(function SelectList2({ options, selectedIndex }) {
151
+ if (options.length === 0) {
152
+ return /* @__PURE__ */ jsx7(Box4, { paddingLeft: 2, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "No options available" }) });
153
+ }
154
+ return /* @__PURE__ */ jsx7(Box4, { flexDirection: "column", paddingLeft: 2, children: options.map((opt, i) => {
155
+ const isSelected = i === selectedIndex;
156
+ return /* @__PURE__ */ jsx7(Box4, { children: /* @__PURE__ */ jsx7(Text7, { color: "cyan", bold: isSelected, inverse: isSelected, children: opt.label }) }, opt.value);
157
+ }) });
158
+ });
159
+
160
+ // src/components/multi-select-list.tsx
161
+ import React5 from "react";
162
+ import { Box as Box5, Text as Text8 } from "ink";
163
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
164
+ var MultiSelectList = React5.memo(function MultiSelectList2({
165
+ options,
166
+ focusedIndex,
167
+ checkedIndices
168
+ }) {
169
+ if (options.length === 0) {
170
+ return /* @__PURE__ */ jsx8(Box5, { paddingLeft: 2, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "No options available" }) });
171
+ }
172
+ const allChecked = checkedIndices.size === options.length;
173
+ const selectAllFocused = focusedIndex === 0;
174
+ return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", paddingLeft: 2, children: [
175
+ /* @__PURE__ */ jsx8(Box5, { children: /* @__PURE__ */ jsxs6(Text8, { color: "cyan", bold: selectAllFocused, inverse: selectAllFocused, children: [
176
+ allChecked ? "[x]" : "[ ]",
177
+ " Select all"
178
+ ] }) }, "select-all"),
179
+ options.map((opt, i) => {
180
+ const isFocused = focusedIndex === i + 1;
181
+ const isChecked = checkedIndices.has(i);
182
+ return /* @__PURE__ */ jsx8(Box5, { children: /* @__PURE__ */ jsxs6(Text8, { color: "cyan", bold: isFocused, inverse: isFocused, children: [
183
+ isChecked ? "[x]" : "[ ]",
184
+ " ",
185
+ opt.label
186
+ ] }) }, opt.value);
187
+ }),
188
+ /* @__PURE__ */ jsx8(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "(Space to toggle, Enter to confirm)" }) })
189
+ ] });
190
+ });
191
+
192
+ // src/components/confirm-prompt.tsx
193
+ import { Box as Box6, Text as Text9 } from "ink";
194
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
195
+ function ConfirmPrompt({ value }) {
196
+ return /* @__PURE__ */ jsxs7(Box6, { gap: 2, paddingLeft: 2, children: [
197
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", bold: value, inverse: value, children: "Yes" }),
198
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", bold: !value, inverse: !value, children: "No" }),
199
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "(\u2190/\u2192 to toggle, Enter to confirm)" })
200
+ ] });
201
+ }
202
+
141
203
  // src/commands/registry.ts
142
204
  import { Fzf } from "fzf";
143
205
 
144
206
  // src/commands/create-application/create-application.command.ts
145
- import { join as join10 } from "path";
146
- import { cwd as cwd2 } from "process";
207
+ import { join as join11, resolve } from "path";
147
208
  import { mkdir as mkdir2 } from "fs/promises";
148
209
 
149
210
  // src/commands/create-application/scaffold-application-monorepo.ts
@@ -250,7 +311,12 @@ async function scaffoldBootstrap(bootstrapServiceDir, organizationName, bootstra
250
311
  {
251
312
  templateDir: bootstrapTemplateDir,
252
313
  outputDir: bootstrapServiceDir,
253
- variables: { organizationName, bootstrapName, bootstrapServiceDir: bootstrapServiceDir_var },
314
+ variables: {
315
+ organizationName,
316
+ bootstrapName,
317
+ bootstrapServiceName: `${bootstrapName}-bootstrap-service`,
318
+ bootstrapServiceDir: bootstrapServiceDir_var
319
+ },
254
320
  exclude: ["src/data/shared-libraries.json", "src/data/platform/modules-config.json"]
255
321
  },
256
322
  (message) => logger.log(message)
@@ -287,7 +353,7 @@ var reactUiModuleTemplateDir = join5(
287
353
  "react-ui-module-template"
288
354
  );
289
355
  async function scaffoldUiModule(uiDir, organizationName, platformName, applicationName, applicationDisplayName, uiBaseDir, logger) {
290
- logger.log(`Creating UI module "${platformName}-${applicationName}-ui"...`);
356
+ logger.log(`Creating UI module "${applicationName}-ui"...`);
291
357
  await applyTemplate(
292
358
  {
293
359
  templateDir: reactUiModuleTemplateDir,
@@ -341,32 +407,34 @@ async function addModuleEntry(bootstrapServiceDir, applicationName, entry, logge
341
407
 
342
408
  // src/commands/create-application/module-entry-builders.ts
343
409
  function buildServiceModuleEntry(organizationName, platformName, applicationName, applicationDisplayName) {
410
+ const serviceName = `${platformName}-${applicationName}-service`;
344
411
  return {
345
- name: `@${organizationName}/${platformName}-${applicationName}-service`,
412
+ name: `@${organizationName}/${serviceName}`,
346
413
  displayName: `${applicationDisplayName} Service`,
347
414
  type: "service",
348
- baseUrl: `/${platformName}-${applicationName}-service`,
415
+ baseUrl: `/${serviceName}`,
349
416
  service: {
350
- host: `${platformName}-${applicationName}-service`,
417
+ host: serviceName,
351
418
  port: 80
352
419
  },
353
420
  dependsOn: []
354
421
  };
355
422
  }
356
- function buildUiModuleEntry(organizationName, platformName, applicationName, applicationDisplayName) {
423
+ function buildUiModuleEntry(organizationName, platformName, applicationName, applicationDisplayName, uiNameOverride) {
424
+ const uiName = uiNameOverride ?? `${platformName}-${applicationName}-ui`;
357
425
  return {
358
- name: `@${organizationName}/${platformName}-${applicationName}-ui`,
426
+ name: `@${organizationName}/${uiName}`,
359
427
  displayName: applicationDisplayName,
360
428
  type: "app",
361
- baseUrl: `/${platformName}-${applicationName}-ui`,
429
+ baseUrl: `/${uiName}`,
362
430
  service: {
363
- host: `${platformName}-${applicationName}-ui`,
431
+ host: uiName,
364
432
  port: 80
365
433
  },
366
434
  ui: {
367
435
  route: `/${applicationName}`,
368
436
  mountAtSelector: "#pae-shell-ui-content",
369
- bundleFile: `${organizationName}-${platformName}-${applicationName}-ui.js`,
437
+ bundleFile: `${organizationName}-${uiName}.js`,
370
438
  customProps: {}
371
439
  },
372
440
  dependsOn: [`@bluealba/pae-shell-ui`]
@@ -376,39 +444,42 @@ function buildUiModuleEntry(organizationName, platformName, applicationName, app
376
444
  // src/commands/create-application/create-docker-compose.ts
377
445
  import { writeFile as writeFile3 } from "fs/promises";
378
446
  function buildBootstrapBlock(platformName, applicationName) {
379
- return ` ${platformName}-${applicationName}-bootstrap-service:
447
+ const bootstrapName = `${platformName}-${applicationName}-bootstrap-service`;
448
+ return ` ${bootstrapName}:
380
449
  build:
381
- context: ../${platformName}-${applicationName}/services/${platformName}-${applicationName}-bootstrap-service
450
+ context: ../../${platformName}-${applicationName}/services/${bootstrapName}
382
451
  dockerfile: Dockerfile.development
383
452
  environment:
384
453
  - NODE_TLS_REJECT_UNAUTHORIZED=0
385
- - SERVICE_ACCESS_NAME=${platformName}-${applicationName}-bootstrap
454
+ - SERVICE_ACCESS_NAME=${applicationName}-bootstrap
386
455
  - WAIT_TIME=5000
387
456
  - SYNC_STRATEGY=\${PAE_BOOTSTRAP_SYNC_STRATEGY}
388
457
  - GATEWAY_SERVICE_URL=\${PAE_GATEWAY_URL}
389
458
  - SERVICE_ACCESS_SECRET=\${PAE_GATEWAY_SERVICE_ACCESS_SECRET}
390
459
  volumes:
391
- - \${PWD}/../:/app/out
460
+ - \${PWD}/:/app/out
392
461
  depends_on:
393
462
  pae-nestjs-gateway-service:
394
463
  condition: service_healthy
395
464
  `;
396
465
  }
397
466
  function buildUiBlock(platformName, applicationName, uiPort) {
398
- return ` ${platformName}-${applicationName}-ui:
467
+ const uiName = `${platformName}-${applicationName}-ui`;
468
+ return ` ${uiName}:
399
469
  build:
400
- context: ../${platformName}-${applicationName}/ui/${platformName}-${applicationName}-ui
470
+ context: ../../${platformName}-${applicationName}/ui/${uiName}
401
471
  dockerfile: Dockerfile.development
402
472
  ports:
403
473
  - ${uiPort}:80
404
474
  volumes:
405
- - \${PWD}/../:/app/out
475
+ - \${PWD}/:/app/out
406
476
  `;
407
477
  }
408
478
  function buildBackendBlock(platformName, applicationName, servicePort) {
409
- return ` ${platformName}-${applicationName}-service:
479
+ const serviceName = `${platformName}-${applicationName}-service`;
480
+ return ` ${serviceName}:
410
481
  build:
411
- context: ../${platformName}-${applicationName}/services/${platformName}-${applicationName}-service
482
+ context: ../../${platformName}-${applicationName}/services/${serviceName}
412
483
  dockerfile: Dockerfile.development
413
484
  args:
414
485
  - BA_NPM_AUTH_TOKEN=$BA_NPM_AUTH_TOKEN
@@ -417,10 +488,10 @@ function buildBackendBlock(platformName, applicationName, servicePort) {
417
488
  environment:
418
489
  - GATEWAY_URL=\${PAE_GATEWAY_URL}
419
490
  volumes:
420
- - \${PWD}/../:/app/out
491
+ - \${PWD}/:/app/out
421
492
  `;
422
493
  }
423
- async function createDockerCompose(dockerComposePath, platformName, applicationName, hasUserInterface, hasBackendService, uiPort, servicePort, logger) {
494
+ async function createDockerCompose(dockerComposePath, applicationName, platformName, hasUserInterface, hasBackendService, uiPort, servicePort, logger) {
424
495
  const blocks = ["services:\n", buildBootstrapBlock(platformName, applicationName)];
425
496
  if (hasUserInterface) {
426
497
  blocks.push(buildUiBlock(platformName, applicationName, uiPort));
@@ -428,7 +499,7 @@ async function createDockerCompose(dockerComposePath, platformName, applicationN
428
499
  if (hasBackendService) {
429
500
  blocks.push(buildBackendBlock(platformName, applicationName, servicePort));
430
501
  }
431
- const content = blocks.join("\n");
502
+ const content = blocks.join("\n") + "\n";
432
503
  try {
433
504
  await writeFile3(dockerComposePath, content, "utf-8");
434
505
  logger.log(`Created docker-compose: ${dockerComposePath}`);
@@ -438,32 +509,17 @@ async function createDockerCompose(dockerComposePath, platformName, applicationN
438
509
  }
439
510
  }
440
511
 
441
- // src/commands/create-application/update-root-docker-compose.ts
442
- import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
443
- async function updateRootDockerCompose(rootDockerComposePath, platformName, applicationName, logger) {
444
- try {
445
- const existing = await readFile3(rootDockerComposePath, "utf-8");
446
- const includeLine = ` - path: ./${platformName}-${applicationName}-docker-compose.yml
447
- `;
448
- await writeFile4(rootDockerComposePath, existing + includeLine, "utf-8");
449
- logger.log(`Updated root docker-compose: ${rootDockerComposePath}`);
450
- } catch (err) {
451
- const message = err instanceof Error ? err.message : String(err);
452
- logger.log(`Warning: Could not update root docker-compose \u2014 ${message}`);
453
- }
454
- }
455
-
456
512
  // src/commands/create-application/port-allocator.ts
457
513
  import { join as join8 } from "path";
458
- import { readdir as readdir2, readFile as readFile4 } from "fs/promises";
514
+ import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
459
515
  async function getNextAvailablePort(localDir) {
460
516
  const allPorts = [];
461
517
  try {
462
518
  const files = await readdir2(localDir);
463
- const dockerComposeFiles = files.filter((f) => f.endsWith("-docker-compose.yml"));
464
- for (const file of dockerComposeFiles) {
519
+ const ymlFiles = files.filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
520
+ for (const file of ymlFiles) {
465
521
  try {
466
- const content = await readFile4(join8(localDir, file), "utf-8");
522
+ const content = await readFile3(join8(localDir, file), "utf-8");
467
523
  const regex = /^\s*-\s*"?(\d+):\d+"?\s*$/gm;
468
524
  let match;
469
525
  while ((match = regex.exec(content)) !== null) {
@@ -482,78 +538,143 @@ function formatError(err) {
482
538
  return err instanceof Error ? err.message : String(err);
483
539
  }
484
540
 
485
- // src/utils/platform-check.ts
486
- import { access, readdir as readdir3, readFile as readFile5 } from "fs/promises";
487
- import { join as join9 } from "path";
488
- import { cwd } from "process";
489
- async function findCoreDirName(dir) {
490
- const entries = await readdir3(dir, { withFileTypes: true });
491
- const coreEntry = entries.find((e) => e.isDirectory() && e.name.endsWith("-core"));
492
- return coreEntry?.name ?? null;
493
- }
494
- async function readPlatformManifest(dir) {
495
- const baseDir = dir ?? cwd();
496
- const coreDirName = await findCoreDirName(baseDir);
497
- if (!coreDirName) {
498
- throw new Error("No *-core directory found \u2014 platform not initialized in this directory.");
541
+ // src/utils/platform-layout.ts
542
+ import { access, readdir as readdir3 } from "fs/promises";
543
+ import { join as join9, dirname as dirname7 } from "path";
544
+ import { cwd as cwd2 } from "process";
545
+ async function findCoreDirIn(dir) {
546
+ let entries;
547
+ try {
548
+ entries = await readdir3(dir, { withFileTypes: true });
549
+ } catch {
550
+ return null;
551
+ }
552
+ const dirs = entries.filter((e) => e.isDirectory());
553
+ for (const entry of dirs) {
554
+ if (entry.name.endsWith("-core")) {
555
+ try {
556
+ await access(join9(dir, entry.name, "product.manifest.json"));
557
+ return entry.name;
558
+ } catch {
559
+ }
560
+ }
561
+ }
562
+ const coreEntry = dirs.find((e) => e.name === "core");
563
+ if (coreEntry) {
564
+ try {
565
+ await access(join9(dir, "core", "product.manifest.json"));
566
+ return "core";
567
+ } catch {
568
+ }
499
569
  }
500
- const platformName = coreDirName.replace(/-core$/, "");
501
- const pkgJson = JSON.parse(await readFile5(join9(baseDir, coreDirName, "package.json"), "utf-8"));
502
- const scopeMatch = pkgJson.name.match(/^@([^/]+)\//);
503
- if (!scopeMatch) {
504
- throw new Error(`Could not parse organization from package name: "${pkgJson.name}"`);
570
+ return null;
571
+ }
572
+ async function findPlatformLayout(startDir = cwd2()) {
573
+ let dir = startDir;
574
+ while (true) {
575
+ const coreDirName = await findCoreDirIn(dir);
576
+ if (coreDirName) {
577
+ const coreDir = join9(dir, coreDirName);
578
+ return {
579
+ rootDir: dir,
580
+ coreDir,
581
+ coreDirName,
582
+ localDir: join9(coreDir, "local")
583
+ };
584
+ }
585
+ const parent = dirname7(dir);
586
+ if (parent === dir) {
587
+ return null;
588
+ }
589
+ dir = parent;
505
590
  }
506
- return { platformName, organizationName: scopeMatch[1] };
507
591
  }
592
+
593
+ // src/utils/platform-check.ts
508
594
  async function isPlatformInitialized() {
509
- try {
510
- const coreDirName = await findCoreDirName(cwd());
511
- if (!coreDirName) return false;
512
- const platformName = coreDirName.replace(/-core$/, "");
513
- await access(join9(cwd(), `${platformName}-local`));
514
- return true;
515
- } catch {
516
- return false;
517
- }
595
+ return await findPlatformLayout() !== null;
596
+ }
597
+
598
+ // src/utils/manifest.ts
599
+ import { readFile as readFile4, writeFile as writeFile4, access as access2 } from "fs/promises";
600
+ import { join as join10 } from "path";
601
+ import { cwd as cwd3 } from "process";
602
+ function manifestPath(rootDir, coreDirName = "core") {
603
+ return join10(rootDir, coreDirName, "product.manifest.json");
604
+ }
605
+ async function readManifest(rootDir = cwd3(), coreDirName = "core") {
606
+ const content = await readFile4(manifestPath(rootDir, coreDirName), "utf-8");
607
+ return JSON.parse(content);
608
+ }
609
+ async function writeManifest(manifest, rootDir = cwd3(), coreDirName = "core") {
610
+ await writeFile4(manifestPath(rootDir, coreDirName), JSON.stringify(manifest, null, 2), "utf-8");
611
+ }
612
+ function createInitialManifest(params) {
613
+ const { organizationName, platformName, platformDisplayName } = params;
614
+ return {
615
+ version: "1",
616
+ product: {
617
+ name: platformName,
618
+ displayName: platformDisplayName,
619
+ organization: organizationName,
620
+ scope: `@${organizationName}`
621
+ },
622
+ applications: []
623
+ };
624
+ }
625
+ function addApplicationToManifest(manifest, app) {
626
+ return {
627
+ ...manifest,
628
+ applications: [...manifest.applications, app]
629
+ };
518
630
  }
519
631
 
520
632
  // src/commands/create-application/create-application.command.ts
521
633
  var CREATE_APPLICATION_COMMAND_NAME = "create-application";
522
634
  var createApplicationCommand = {
523
635
  name: CREATE_APPLICATION_COMMAND_NAME,
524
- description: "Create an application in a platform"
636
+ description: "Create an application in a platform",
637
+ hidden: (ctx) => !ctx.platformInitialized
525
638
  };
526
639
  async function createApplication(params, logger) {
527
640
  const {
528
- organizationName,
529
- platformName,
530
641
  applicationName,
531
642
  applicationDisplayName,
532
643
  applicationDescription,
533
644
  hasUserInterface,
534
645
  hasBackendService
535
646
  } = params;
536
- if (!await isPlatformInitialized()) {
647
+ const layout = await findPlatformLayout();
648
+ if (!layout) {
537
649
  logger.log("Error: Cannot create an application \u2014 no platform initialized in this directory.");
538
650
  return;
539
651
  }
540
- const rootDir = cwd2();
541
- const applicationDir = join10(rootDir, `${platformName}-${applicationName}`);
542
- const bootstrapServiceDir = join10(applicationDir, "services", `${platformName}-${applicationName}-bootstrap-service`);
543
- const localDir = join10(rootDir, `${platformName}-local`);
544
- logger.log(`Creating application monorepo "${platformName}-${applicationName}"...`);
652
+ const { rootDir, coreDirName, localDir } = layout;
653
+ let manifest;
654
+ try {
655
+ manifest = await readManifest(rootDir, coreDirName);
656
+ } catch (err) {
657
+ logger.log(`Error: Could not read product manifest \u2014 ${formatError(err)}`);
658
+ return;
659
+ }
660
+ const { organization: organizationName, name: platformName } = manifest.product;
661
+ const localPath = `../${platformName}-${applicationName}`;
662
+ const applicationDir = resolve(join11(rootDir, coreDirName), localPath);
663
+ const bootstrapServiceName = `${platformName}-${applicationName}-bootstrap-service`;
664
+ const bootstrapServiceDir = join11(applicationDir, "services", bootstrapServiceName);
665
+ logger.log(`Creating application monorepo "${applicationName}"...`);
545
666
  try {
546
667
  await scaffoldApplicationMonorepo(applicationDir, organizationName, platformName, applicationName, logger);
547
668
  } catch (err) {
548
669
  logger.log(`Error: Could not scaffold application monorepo \u2014 ${formatError(err)}`);
549
670
  return;
550
671
  }
551
- await mkdir2(join10(applicationDir, "services"), { recursive: true });
552
- await mkdir2(join10(applicationDir, "ui"), { recursive: true });
553
- logger.log(`Creating bootstrap service "${platformName}-${applicationName}-bootstrap-service"...`);
554
- const bootstrapServiceDir_var = `${platformName}-${applicationName}/services`;
672
+ await mkdir2(join11(applicationDir, "services"), { recursive: true });
673
+ await mkdir2(join11(applicationDir, "ui"), { recursive: true });
674
+ logger.log(`Creating bootstrap service "${bootstrapServiceName}"...`);
675
+ const bootstrapServiceBaseDir = `${platformName}-${applicationName}/services`;
555
676
  try {
556
- await scaffoldBootstrap(bootstrapServiceDir, organizationName, `${platformName}-${applicationName}`, bootstrapServiceDir_var, logger);
677
+ await scaffoldBootstrap(bootstrapServiceDir, organizationName, `${platformName}-${applicationName}`, bootstrapServiceBaseDir, logger);
557
678
  } catch (err) {
558
679
  logger.log(`Error: Could not scaffold bootstrap service \u2014 ${formatError(err)}`);
559
680
  return;
@@ -582,7 +703,8 @@ async function createApplication(params, logger) {
582
703
  );
583
704
  }
584
705
  if (hasUserInterface) {
585
- const uiDir = join10(applicationDir, "ui", `${platformName}-${applicationName}-ui`);
706
+ const uiName = `${platformName}-${applicationName}-ui`;
707
+ const uiDir = join11(applicationDir, "ui", uiName);
586
708
  const uiBaseDir = `${platformName}-${applicationName}/ui`;
587
709
  try {
588
710
  await scaffoldUiModule(uiDir, organizationName, platformName, applicationName, applicationDisplayName, uiBaseDir, logger);
@@ -592,7 +714,8 @@ async function createApplication(params, logger) {
592
714
  }
593
715
  }
594
716
  if (hasBackendService) {
595
- const serviceDir = join10(applicationDir, "services", `${platformName}-${applicationName}-service`);
717
+ const serviceName = `${platformName}-${applicationName}-service`;
718
+ const serviceDir = join11(applicationDir, "services", serviceName);
596
719
  const serviceBaseDir = `${platformName}-${applicationName}/services`;
597
720
  try {
598
721
  await scaffoldNestjsService(serviceDir, organizationName, platformName, applicationName, applicationDisplayName, serviceBaseDir, logger);
@@ -601,28 +724,39 @@ async function createApplication(params, logger) {
601
724
  return;
602
725
  }
603
726
  }
604
- const dockerComposePath = join10(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
727
+ const dockerComposePath = join11(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
605
728
  const basePort = await getNextAvailablePort(localDir);
606
729
  const uiPort = hasUserInterface ? basePort : 0;
607
730
  const servicePort = hasBackendService ? hasUserInterface ? basePort + 1 : basePort : 0;
608
731
  await createDockerCompose(
609
732
  dockerComposePath,
610
- platformName,
611
733
  applicationName,
734
+ platformName,
612
735
  hasUserInterface,
613
736
  hasBackendService,
614
737
  uiPort,
615
738
  servicePort,
616
739
  logger
617
740
  );
618
- const rootDockerComposePath = join10(localDir, "docker-compose.yml");
619
- await updateRootDockerCompose(rootDockerComposePath, platformName, applicationName, logger);
620
- logger.log(`Done! Application "${platformName}-${applicationName}" created at ${applicationDir}`);
741
+ const updatedManifest = addApplicationToManifest(manifest, {
742
+ name: applicationName,
743
+ displayName: applicationDisplayName,
744
+ description: applicationDescription,
745
+ localPath,
746
+ repository: null
747
+ });
748
+ try {
749
+ await writeManifest(updatedManifest, rootDir, coreDirName);
750
+ logger.log(`Updated product manifest with application "${applicationName}".`);
751
+ } catch (err) {
752
+ logger.log(`Warning: Could not update product manifest \u2014 ${formatError(err)}`);
753
+ }
754
+ logger.log(`Done! Application "${applicationName}" created at ${applicationDir}`);
621
755
  }
622
756
 
623
757
  // src/commands/init/init.command.ts
624
- import { join as join16 } from "path";
625
- import { cwd as cwd3 } from "process";
758
+ import { join as join17 } from "path";
759
+ import { cwd as cwd4 } from "process";
626
760
 
627
761
  // src/utils/string.ts
628
762
  function camelize(name) {
@@ -631,9 +765,9 @@ function camelize(name) {
631
765
 
632
766
  // src/commands/init/scaffold-platform.ts
633
767
  import { fileURLToPath as fileURLToPath6 } from "url";
634
- import { join as join11, dirname as dirname7 } from "path";
635
- var templateDir = join11(
636
- dirname7(fileURLToPath6(import.meta.url)),
768
+ import { join as join12, dirname as dirname8 } from "path";
769
+ var templateDir = join12(
770
+ dirname8(fileURLToPath6(import.meta.url)),
637
771
  "..",
638
772
  "templates",
639
773
  "platform-init-template"
@@ -644,9 +778,9 @@ async function scaffoldPlatform(outputDir, variables, logger) {
644
778
 
645
779
  // src/commands/init/scaffold-platform-bootstrap.ts
646
780
  import { fileURLToPath as fileURLToPath7 } from "url";
647
- import { join as join12, dirname as dirname8 } from "path";
648
- var templateDir2 = join12(
649
- dirname8(fileURLToPath7(import.meta.url)),
781
+ import { join as join13, dirname as dirname9 } from "path";
782
+ var templateDir2 = join13(
783
+ dirname9(fileURLToPath7(import.meta.url)),
650
784
  "..",
651
785
  "templates",
652
786
  "bootstrap-service-template"
@@ -657,9 +791,9 @@ async function scaffoldPlatformBootstrap(outputDir, variables, logger) {
657
791
 
658
792
  // src/commands/init/scaffold-customization-ui.ts
659
793
  import { fileURLToPath as fileURLToPath8 } from "url";
660
- import { join as join13, dirname as dirname9 } from "path";
661
- var templateDir3 = join13(
662
- dirname9(fileURLToPath8(import.meta.url)),
794
+ import { join as join14, dirname as dirname10 } from "path";
795
+ var templateDir3 = join14(
796
+ dirname10(fileURLToPath8(import.meta.url)),
663
797
  "..",
664
798
  "templates",
665
799
  "customization-ui-module-template"
@@ -669,33 +803,34 @@ async function scaffoldCustomizationUi(outputDir, variables, logger) {
669
803
  }
670
804
 
671
805
  // src/commands/init/register-customization-module.ts
672
- import { join as join14 } from "path";
673
- import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
674
- async function registerCustomizationModule(bootstrapServiceDir, organizationName, platformName, logger) {
675
- const modulesJsonPath = join14(bootstrapServiceDir, "src", "data", "platform", "modules.json");
806
+ import { join as join15 } from "path";
807
+ import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
808
+ async function registerCustomizationModule(bootstrapServiceDir, organizationName, logger, platformName = "platform") {
809
+ const modulesJsonPath = join15(bootstrapServiceDir, "src", "data", "platform", "modules.json");
810
+ const customizationUiName = `${platformName}-customization-ui`;
676
811
  const entry = {
677
- name: `@${organizationName}/${platformName}-customization-ui`,
812
+ name: `@${organizationName}/${customizationUiName}`,
678
813
  displayName: "Platform Customization UI",
679
814
  type: "app",
680
- baseUrl: `/${platformName}-customization-ui`,
681
- service: { host: `${platformName}-customization-ui`, port: 80 },
815
+ baseUrl: `/${customizationUiName}`,
816
+ service: { host: customizationUiName, port: 80 },
682
817
  ui: {
683
818
  route: "/",
684
- bundleFile: `${platformName}-customization-ui.js`,
819
+ bundleFile: `${customizationUiName}.js`,
685
820
  isPlatformCustomization: true,
686
821
  customProps: {}
687
822
  },
688
823
  dependsOn: []
689
824
  };
690
- const existing = JSON.parse(await readFile6(modulesJsonPath, "utf-8"));
825
+ const existing = JSON.parse(await readFile5(modulesJsonPath, "utf-8"));
691
826
  existing.push(entry);
692
827
  await writeFile5(modulesJsonPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
693
828
  logger.log(`Registered customization module in ${modulesJsonPath}`);
694
829
  }
695
830
 
696
831
  // src/commands/init/generate-local-env.ts
697
- import { readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
698
- import { join as join15 } from "path";
832
+ import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
833
+ import { join as join16 } from "path";
699
834
 
700
835
  // src/utils/random.ts
701
836
  import { randomBytes } from "crypto";
@@ -704,11 +839,11 @@ function generateRandomSecret() {
704
839
  }
705
840
 
706
841
  // src/commands/init/generate-local-env.ts
707
- async function generateLocalEnv(outputDir, platformName, logger) {
708
- const examplePath = join15(outputDir, `${platformName}-local`, ".env.example");
709
- const envPath = join15(outputDir, `${platformName}-local`, ".env");
710
- logger.log(`Generating ${platformName}-local/.env with random secrets...`);
711
- const content = await readFile7(examplePath, "utf-8");
842
+ async function generateLocalEnv(outputDir, logger, coreDirName = "core") {
843
+ const examplePath = join16(outputDir, coreDirName, "local", ".env.example");
844
+ const envPath = join16(outputDir, coreDirName, "local", ".env");
845
+ logger.log(`Generating ${coreDirName}/local/.env with random secrets...`);
846
+ const content = await readFile6(examplePath, "utf-8");
712
847
  const result = content.replace(/^(PAE_AUTH_JWT_SECRET|PAE_GATEWAY_SERVICE_ACCESS_SECRET|PAE_DB_PASSWORD)=$/gm, (_, key) => {
713
848
  return `${key}=${generateRandomSecret()}`;
714
849
  });
@@ -719,7 +854,8 @@ async function generateLocalEnv(outputDir, platformName, logger) {
719
854
  var INIT_COMMAND_NAME = "init";
720
855
  var initCommand = {
721
856
  name: INIT_COMMAND_NAME,
722
- description: "Initialize a new platform"
857
+ description: "Initialize a new platform",
858
+ hidden: ({ platformInitialized }) => platformInitialized
723
859
  };
724
860
  async function init(params, logger) {
725
861
  if (await isPlatformInitialized()) {
@@ -728,15 +864,22 @@ async function init(params, logger) {
728
864
  }
729
865
  const { organizationName, platformName, platformDisplayName } = params;
730
866
  const platformTitle = camelize(platformName);
731
- const outputDir = cwd3();
867
+ const outputDir = cwd4();
732
868
  logger.log(`Initializing ${platformTitle} platform for @${organizationName}...`);
869
+ const coreDirName = `${platformName}-core`;
870
+ const bootstrapSuffix = params.bootstrapServiceSuffix ?? "bootstrap-service";
871
+ const customizationUiSuffix = params.customizationUiSuffix ?? "customization-ui";
872
+ const bootstrapServiceName = `${platformName}-${bootstrapSuffix}`;
873
+ const customizationUiName = `${platformName}-${customizationUiSuffix}`;
733
874
  const variables = {
734
875
  organizationName,
735
876
  platformName,
736
877
  platformTitle,
737
878
  platformDisplayName,
738
879
  bootstrapName: platformName,
739
- bootstrapServiceDir: `${platformName}-core/services`
880
+ bootstrapServiceName,
881
+ customizationUiName,
882
+ bootstrapServiceDir: `${coreDirName}/services`
740
883
  };
741
884
  try {
742
885
  await scaffoldPlatform(outputDir, variables, logger);
@@ -745,14 +888,22 @@ async function init(params, logger) {
745
888
  return;
746
889
  }
747
890
  try {
748
- await generateLocalEnv(outputDir, platformName, logger);
891
+ await generateLocalEnv(outputDir, logger, coreDirName);
892
+ } catch (err) {
893
+ logger.log(`Error: Could not generate ${coreDirName}/local/.env \u2014 ${formatError(err)}`);
894
+ return;
895
+ }
896
+ try {
897
+ const manifest = createInitialManifest({ organizationName, platformName, platformDisplayName });
898
+ await writeManifest(manifest, outputDir, coreDirName);
899
+ logger.log(`Created product manifest: ${coreDirName}/product.manifest.json`);
749
900
  } catch (err) {
750
- logger.log(`Error: Could not generate ${platformName}-local/.env \u2014 ${formatError(err)}`);
901
+ logger.log(`Error: Could not write product manifest \u2014 ${formatError(err)}`);
751
902
  return;
752
903
  }
753
904
  try {
754
905
  await scaffoldPlatformBootstrap(
755
- join16(outputDir, `${platformName}-core`, "services", `${platformName}-bootstrap-service`),
906
+ join17(outputDir, coreDirName, "services", bootstrapServiceName),
756
907
  variables,
757
908
  logger
758
909
  );
@@ -762,7 +913,7 @@ async function init(params, logger) {
762
913
  }
763
914
  try {
764
915
  await scaffoldCustomizationUi(
765
- join16(outputDir, `${platformName}-core`, "ui", `${platformName}-customization-ui`),
916
+ join17(outputDir, coreDirName, "ui", customizationUiName),
766
917
  variables,
767
918
  logger
768
919
  );
@@ -772,10 +923,10 @@ async function init(params, logger) {
772
923
  }
773
924
  try {
774
925
  await registerCustomizationModule(
775
- join16(outputDir, `${platformName}-core`, "services", `${platformName}-bootstrap-service`),
926
+ join17(outputDir, coreDirName, "services", bootstrapServiceName),
776
927
  organizationName,
777
- platformName,
778
- logger
928
+ logger,
929
+ platformName
779
930
  );
780
931
  } catch (err) {
781
932
  logger.log(`Error: Could not register customization module \u2014 ${formatError(err)}`);
@@ -785,16 +936,15 @@ async function init(params, logger) {
785
936
  }
786
937
 
787
938
  // src/commands/configure-idp/configure-idp.command.ts
788
- import { join as join17 } from "path";
789
- import { cwd as cwd4 } from "process";
939
+ import { join as join18 } from "path";
790
940
  import { fetch as undiciFetch, Agent } from "undici";
791
941
 
792
942
  // src/utils/env-reader.ts
793
- import { readFile as readFile8 } from "fs/promises";
943
+ import { readFile as readFile7 } from "fs/promises";
794
944
  async function readEnvFile(filePath) {
795
945
  let content;
796
946
  try {
797
- content = await readFile8(filePath, "utf-8");
947
+ content = await readFile7(filePath, "utf-8");
798
948
  } catch (error) {
799
949
  if (error.code === "ENOENT") {
800
950
  throw new Error(`.env file not found at ${filePath}`);
@@ -861,15 +1011,16 @@ function getAllProviders() {
861
1011
  var CONFIGURE_IDP_COMMAND_NAME = "configure-idp";
862
1012
  var configureIdpCommand = {
863
1013
  name: CONFIGURE_IDP_COMMAND_NAME,
864
- description: "Configure an Identity Provider (IDP) in the gateway"
1014
+ description: "Configure an Identity Provider (IDP) in the gateway",
1015
+ hidden: (ctx) => !ctx.platformInitialized
865
1016
  };
866
1017
  async function configureIdp(params, logger) {
867
- if (!await isPlatformInitialized()) {
1018
+ const layout = await findPlatformLayout();
1019
+ if (!layout) {
868
1020
  logger.log("Error: Cannot configure an IDP \u2014 no platform initialized in this directory.");
869
1021
  return;
870
1022
  }
871
- const { platformName } = await readPlatformManifest();
872
- const envPath = join17(cwd4(), `${platformName}-local`, ".env");
1023
+ const envPath = join18(layout.localDir, ".env");
873
1024
  let env;
874
1025
  try {
875
1026
  env = await readEnvFile(envPath);
@@ -880,11 +1031,11 @@ async function configureIdp(params, logger) {
880
1031
  const gatewayUrl = env.get("PAE_GATEWAY_HOST_URL");
881
1032
  const accessSecret = env.get("PAE_GATEWAY_SERVICE_ACCESS_SECRET");
882
1033
  if (!gatewayUrl) {
883
- logger.log(`Error: PAE_GATEWAY_HOST_URL is not set in ${platformName}-local/.env`);
1034
+ logger.log("Error: PAE_GATEWAY_HOST_URL is not set in core/local/.env");
884
1035
  return;
885
1036
  }
886
1037
  if (!accessSecret) {
887
- logger.log(`Error: PAE_GATEWAY_SERVICE_ACCESS_SECRET is not set in ${platformName}-local/.env`);
1038
+ logger.log("Error: PAE_GATEWAY_SERVICE_ACCESS_SECRET is not set in core/local/.env");
888
1039
  return;
889
1040
  }
890
1041
  const provider = idpProviderRegistry.get(params.providerType);
@@ -921,15 +1072,14 @@ async function configureIdp(params, logger) {
921
1072
  }
922
1073
 
923
1074
  // src/commands/create-service-module/create-service-module.command.ts
924
- import { join as join19 } from "path";
925
- import { cwd as cwd5 } from "process";
926
- import { access as access2 } from "fs/promises";
1075
+ import { join as join20, resolve as resolve2 } from "path";
1076
+ import { access as access3 } from "fs/promises";
927
1077
 
928
1078
  // src/commands/create-service-module/scaffold-service-module.ts
929
1079
  import { fileURLToPath as fileURLToPath9 } from "url";
930
- import { join as join18, dirname as dirname10 } from "path";
931
- var nestjsServiceModuleTemplateDir2 = join18(
932
- dirname10(fileURLToPath9(import.meta.url)),
1080
+ import { join as join19, dirname as dirname11 } from "path";
1081
+ var nestjsServiceModuleTemplateDir2 = join19(
1082
+ dirname11(fileURLToPath9(import.meta.url)),
933
1083
  "..",
934
1084
  "templates",
935
1085
  "nestjs-service-module-template"
@@ -963,12 +1113,12 @@ function buildCustomServiceModuleEntry(organizationName, serviceName, serviceDis
963
1113
  }
964
1114
 
965
1115
  // src/commands/create-service-module/append-docker-compose.ts
966
- import { readFile as readFile9, writeFile as writeFile7 } from "fs/promises";
1116
+ import { readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
967
1117
  async function appendServiceToDockerCompose(dockerComposePath, serviceName, platformName, applicationName, port, logger) {
968
1118
  const block = `
969
1119
  ${serviceName}:
970
1120
  build:
971
- context: ../${platformName}-${applicationName}/services/${serviceName}
1121
+ context: ../../${platformName}-${applicationName}/services/${serviceName}
972
1122
  dockerfile: Dockerfile.development
973
1123
  args:
974
1124
  - BA_NPM_AUTH_TOKEN=$BA_NPM_AUTH_TOKEN
@@ -977,10 +1127,10 @@ async function appendServiceToDockerCompose(dockerComposePath, serviceName, plat
977
1127
  environment:
978
1128
  - GATEWAY_URL=\${PAE_GATEWAY_URL}
979
1129
  volumes:
980
- - \${PWD}/../:/app/out
1130
+ - \${PWD}/:/app/out
981
1131
  `;
982
1132
  try {
983
- const existing = await readFile9(dockerComposePath, "utf-8");
1133
+ const existing = await readFile8(dockerComposePath, "utf-8");
984
1134
  await writeFile7(dockerComposePath, existing + block, "utf-8");
985
1135
  logger.log(`Updated docker-compose: ${dockerComposePath}`);
986
1136
  } catch (err) {
@@ -993,37 +1143,43 @@ async function appendServiceToDockerCompose(dockerComposePath, serviceName, plat
993
1143
  var CREATE_SERVICE_MODULE_COMMAND_NAME = "create-service-module";
994
1144
  var createServiceModuleCommand = {
995
1145
  name: CREATE_SERVICE_MODULE_COMMAND_NAME,
996
- description: "Add a new service module to an existing application"
1146
+ description: "Add a new service module to an existing application",
1147
+ hidden: (ctx) => !ctx.platformInitialized
997
1148
  };
998
1149
  async function createServiceModule(params, logger) {
999
- const { applicationName, serviceName, serviceDisplayName } = params;
1000
- if (!await isPlatformInitialized()) {
1150
+ const { applicationName, serviceName, serviceDisplayName, serviceNameSuffix } = params;
1151
+ const layout = await findPlatformLayout();
1152
+ if (!layout) {
1001
1153
  logger.log("Error: Cannot create a service module \u2014 no platform initialized in this directory.");
1002
1154
  return;
1003
1155
  }
1004
- const rootDir = cwd5();
1005
- let organizationName;
1006
- let platformName;
1156
+ const { rootDir, coreDirName, localDir } = layout;
1157
+ let manifest;
1007
1158
  try {
1008
- const manifest = await readPlatformManifest(rootDir);
1009
- organizationName = manifest.organizationName;
1010
- platformName = manifest.platformName;
1159
+ manifest = await readManifest(rootDir, coreDirName);
1011
1160
  } catch (err) {
1012
- logger.log(`Error: Could not read .platform.json \u2014 ${formatError(err)}`);
1161
+ logger.log(`Error: Could not read product manifest \u2014 ${formatError(err)}`);
1162
+ return;
1163
+ }
1164
+ const { organization: organizationName, name: platformName } = manifest.product;
1165
+ const appEntry = manifest.applications.find((a) => a.name === applicationName);
1166
+ if (!appEntry) {
1167
+ logger.log(`Error: The specified application "${applicationName}" is not registered in the product manifest.`);
1013
1168
  return;
1014
1169
  }
1015
- const applicationDir = join19(rootDir, `${platformName}-${applicationName}`);
1170
+ const applicationDir = resolve2(join20(rootDir, coreDirName), appEntry.localPath);
1016
1171
  try {
1017
- await access2(applicationDir);
1172
+ await access3(applicationDir);
1018
1173
  } catch {
1019
- logger.log(`Error: The specified application "${platformName}-${applicationName}" does not exist in the platform.`);
1174
+ logger.log(`Error: The specified application "${applicationName}" does not exist in the platform.`);
1020
1175
  return;
1021
1176
  }
1022
- const fullServiceName = `${platformName}-${serviceName}-service`;
1023
- const serviceDir = join19(applicationDir, "services", fullServiceName);
1177
+ const suffix = serviceNameSuffix === void 0 ? "service" : serviceNameSuffix;
1178
+ const fullServiceName = suffix ? `${platformName}-${serviceName}-${suffix}` : `${platformName}-${serviceName}`;
1179
+ const serviceDir = join20(applicationDir, "services", fullServiceName);
1024
1180
  try {
1025
- await access2(serviceDir);
1026
- logger.log(`Error: A service named "${fullServiceName}" already exists in application "${platformName}-${applicationName}".`);
1181
+ await access3(serviceDir);
1182
+ logger.log(`Error: A service named "${fullServiceName}" already exists in application "${applicationName}".`);
1027
1183
  return;
1028
1184
  } catch {
1029
1185
  }
@@ -1041,36 +1197,35 @@ async function createServiceModule(params, logger) {
1041
1197
  logger.log(`Error: Could not scaffold NestJS service \u2014 ${formatError(err)}`);
1042
1198
  return;
1043
1199
  }
1044
- const bootstrapServiceDir = join19(applicationDir, "services", `${platformName}-${applicationName}-bootstrap-service`);
1200
+ const bootstrapServiceDir = join20(applicationDir, "services", `${platformName}-${applicationName}-bootstrap-service`);
1045
1201
  const moduleEntry = buildCustomServiceModuleEntry(organizationName, fullServiceName, serviceDisplayName);
1046
1202
  await addModuleEntry(bootstrapServiceDir, applicationName, moduleEntry, logger);
1047
- const localDir = join19(rootDir, `${platformName}-local`);
1048
- const dockerComposePath = join19(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
1203
+ const dockerComposePath = join20(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
1049
1204
  const port = await getNextAvailablePort(localDir);
1050
1205
  await appendServiceToDockerCompose(dockerComposePath, fullServiceName, platformName, applicationName, port, logger);
1051
- logger.log(`Done! Service module "${fullServiceName}" added to application "${platformName}-${applicationName}".`);
1206
+ logger.log(`Done! Service module "${fullServiceName}" added to application "${applicationName}".`);
1052
1207
  }
1053
1208
 
1054
1209
  // src/commands/create-ui-module/create-ui-module.command.ts
1055
- import { join as join20 } from "path";
1056
- import { cwd as cwd6 } from "process";
1057
- import { access as access3, readdir as readdir4 } from "fs/promises";
1210
+ import { join as join21, resolve as resolve3 } from "path";
1211
+ import { access as access4, readdir as readdir4 } from "fs/promises";
1058
1212
 
1059
1213
  // src/commands/create-ui-module/append-ui-docker-compose.ts
1060
- import { readFile as readFile10, writeFile as writeFile8 } from "fs/promises";
1061
- async function appendUiToDockerCompose(dockerComposePath, platformName, applicationName, port, logger) {
1214
+ import { readFile as readFile9, writeFile as writeFile8 } from "fs/promises";
1215
+ async function appendUiToDockerCompose(dockerComposePath, platformName, applicationName, port, logger, uiNameOverride) {
1216
+ const uiName = uiNameOverride ?? `${platformName}-${applicationName}-ui`;
1062
1217
  const block = `
1063
- ${platformName}-${applicationName}-ui:
1218
+ ${uiName}:
1064
1219
  build:
1065
- context: ../${platformName}-${applicationName}/ui/${platformName}-${applicationName}-ui
1220
+ context: ../../${platformName}-${applicationName}/ui/${uiName}
1066
1221
  dockerfile: Dockerfile.development
1067
1222
  ports:
1068
1223
  - ${port}:80
1069
1224
  volumes:
1070
- - \${PWD}/../:/app/out
1225
+ - \${PWD}/:/app/out
1071
1226
  `;
1072
1227
  try {
1073
- const existing = await readFile10(dockerComposePath, "utf-8");
1228
+ const existing = await readFile9(dockerComposePath, "utf-8");
1074
1229
  await writeFile8(dockerComposePath, existing + block, "utf-8");
1075
1230
  logger.log(`Updated docker-compose: ${dockerComposePath}`);
1076
1231
  } catch (err) {
@@ -1083,43 +1238,50 @@ async function appendUiToDockerCompose(dockerComposePath, platformName, applicat
1083
1238
  var CREATE_UI_MODULE_COMMAND_NAME = "create-ui-module";
1084
1239
  var createUiModuleCommand = {
1085
1240
  name: CREATE_UI_MODULE_COMMAND_NAME,
1086
- description: "Add a UI module to an existing application"
1241
+ description: "Add a UI module to an existing application",
1242
+ hidden: (ctx) => !ctx.platformInitialized
1087
1243
  };
1088
1244
  async function createUiModule(params, logger) {
1089
- const { applicationName, applicationDisplayName } = params;
1090
- if (!await isPlatformInitialized()) {
1245
+ const { applicationName, applicationDisplayName, uiModuleSuffix } = params;
1246
+ const layout = await findPlatformLayout();
1247
+ if (!layout) {
1091
1248
  logger.log("Error: Cannot create a UI module \u2014 no platform initialized in this directory.");
1092
1249
  return;
1093
1250
  }
1094
- const rootDir = cwd6();
1095
- let organizationName;
1096
- let platformName;
1251
+ const { rootDir, coreDirName, localDir } = layout;
1252
+ let manifest;
1097
1253
  try {
1098
- const manifest = await readPlatformManifest(rootDir);
1099
- organizationName = manifest.organizationName;
1100
- platformName = manifest.platformName;
1254
+ manifest = await readManifest(rootDir, coreDirName);
1101
1255
  } catch (err) {
1102
- logger.log(`Error: Could not read .platform.json \u2014 ${formatError(err)}`);
1256
+ logger.log(`Error: Could not read product manifest \u2014 ${formatError(err)}`);
1103
1257
  return;
1104
1258
  }
1105
- const applicationDir = join20(rootDir, `${platformName}-${applicationName}`);
1259
+ const { organization: organizationName, name: platformName } = manifest.product;
1260
+ const appEntry = manifest.applications.find((a) => a.name === applicationName);
1261
+ if (!appEntry) {
1262
+ logger.log(`Error: The specified application "${applicationName}" is not registered in the product manifest.`);
1263
+ return;
1264
+ }
1265
+ const applicationDir = resolve3(join21(rootDir, coreDirName), appEntry.localPath);
1106
1266
  try {
1107
- await access3(applicationDir);
1267
+ await access4(applicationDir);
1108
1268
  } catch {
1109
- logger.log(`Error: The specified application "${platformName}-${applicationName}" does not exist in the platform.`);
1269
+ logger.log(`Error: The specified application "${applicationName}" does not exist in the platform.`);
1110
1270
  return;
1111
1271
  }
1112
- const uiDir = join20(applicationDir, "ui");
1272
+ const uiDir = join21(applicationDir, "ui");
1113
1273
  try {
1114
1274
  const uiEntries = await readdir4(uiDir);
1115
1275
  const existingUiModules = uiEntries.filter((e) => e !== ".gitkeep");
1116
1276
  if (existingUiModules.length > 0) {
1117
- logger.log(`Error: Currently we support only one UI module per application. Application "${platformName}-${applicationName}" already has a UI module.`);
1277
+ logger.log(`Error: Currently we support only one UI module per application. Application "${applicationName}" already has a UI module.`);
1118
1278
  return;
1119
1279
  }
1120
1280
  } catch {
1121
1281
  }
1122
- const uiOutputDir = join20(uiDir, `${platformName}-${applicationName}-ui`);
1282
+ const uiSuffix = uiModuleSuffix === void 0 ? "ui" : uiModuleSuffix;
1283
+ const uiName = uiSuffix ? `${platformName}-${applicationName}-${uiSuffix}` : `${platformName}-${applicationName}`;
1284
+ const uiOutputDir = join21(uiDir, uiName);
1123
1285
  const uiBaseDir = `${platformName}-${applicationName}/ui`;
1124
1286
  try {
1125
1287
  await scaffoldUiModule(uiOutputDir, organizationName, platformName, applicationName, applicationDisplayName, uiBaseDir, logger);
@@ -1127,123 +1289,1069 @@ async function createUiModule(params, logger) {
1127
1289
  logger.log(`Error: Could not scaffold UI module \u2014 ${formatError(err)}`);
1128
1290
  return;
1129
1291
  }
1130
- const bootstrapServiceDir = join20(applicationDir, "services", `${platformName}-${applicationName}-bootstrap-service`);
1131
- const moduleEntry = buildUiModuleEntry(organizationName, platformName, applicationName, applicationDisplayName);
1292
+ const bootstrapServiceDir = join21(applicationDir, "services", `${platformName}-${applicationName}-bootstrap-service`);
1293
+ const moduleEntry = buildUiModuleEntry(organizationName, platformName, applicationName, applicationDisplayName, uiName);
1132
1294
  await addModuleEntry(bootstrapServiceDir, applicationName, moduleEntry, logger);
1133
- const localDir = join20(rootDir, `${platformName}-local`);
1134
- const dockerComposePath = join20(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
1295
+ const dockerComposePath = join21(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
1135
1296
  const port = await getNextAvailablePort(localDir);
1136
- await appendUiToDockerCompose(dockerComposePath, platformName, applicationName, port, logger);
1137
- logger.log(`Done! UI module "${platformName}-${applicationName}-ui" added to application "${platformName}-${applicationName}".`);
1297
+ await appendUiToDockerCompose(dockerComposePath, platformName, applicationName, port, logger, uiName);
1298
+ logger.log(`Done! UI module "${uiName}" added to application "${applicationName}".`);
1138
1299
  }
1139
1300
 
1140
- // src/commands/registry.ts
1141
- var CommandRegistry = class {
1142
- commands = /* @__PURE__ */ new Map();
1143
- register(command) {
1144
- this.commands.set(command.name, command);
1145
- }
1146
- get(name) {
1147
- return this.commands.get(name);
1148
- }
1149
- getAll() {
1150
- return Array.from(this.commands.values());
1151
- }
1152
- search(query) {
1153
- const all = this.getAll();
1154
- if (!query) return all;
1155
- const fzf = new Fzf(all, {
1156
- selector: (cmd) => `${cmd.name} ${cmd.description}`
1157
- });
1158
- return fzf.find(query).map((result) => result.item);
1159
- }
1160
- };
1161
- var registry = new CommandRegistry();
1162
- registry.register(createApplicationCommand);
1163
- registry.register(initCommand);
1164
- registry.register(configureIdpCommand);
1165
- registry.register(createServiceModuleCommand);
1166
- registry.register(createUiModuleCommand);
1167
-
1168
- // src/app-state.ts
1169
- var APP_STATE = {
1170
- IDLE: "idle",
1171
- PALETTE: "palette",
1172
- EXECUTING: "executing",
1173
- PROMPTING: "prompting"
1174
- };
1175
-
1176
- // src/hooks/use-command-runner.ts
1177
- import { useState as useState2, useCallback, useRef } from "react";
1301
+ // src/commands/status/status-checks.ts
1302
+ import { spawn as spawn3 } from "child_process";
1303
+ import { access as access7 } from "fs/promises";
1304
+ import { join as join24, resolve as resolve5 } from "path";
1178
1305
 
1179
- // src/services/create-application.service.ts
1180
- async function createApplicationService(params, logger) {
1181
- await createApplication(params, logger);
1306
+ // src/commands/local-scripts/docker-compose-orchestrator.ts
1307
+ import { spawn } from "child_process";
1308
+ import { access as access5 } from "fs/promises";
1309
+ import { join as join22 } from "path";
1310
+ function runDockerCompose(args2, logger, rootDir, signal) {
1311
+ return new Promise((resolvePromise) => {
1312
+ const child = spawn("docker", ["compose", ...args2], {
1313
+ shell: false,
1314
+ stdio: ["ignore", "pipe", "pipe"],
1315
+ cwd: rootDir
1316
+ });
1317
+ const onAbort = () => {
1318
+ child.kill("SIGTERM");
1319
+ };
1320
+ if (signal) {
1321
+ if (signal.aborted) {
1322
+ child.kill("SIGTERM");
1323
+ resolvePromise();
1324
+ return;
1325
+ }
1326
+ signal.addEventListener("abort", onAbort, { once: true });
1327
+ }
1328
+ child.stdout.on("data", (data) => {
1329
+ for (const line of data.toString().split("\n")) {
1330
+ if (line.trim()) logger.log(line);
1331
+ }
1332
+ });
1333
+ child.stderr.on("data", (data) => {
1334
+ for (const line of data.toString().split("\n")) {
1335
+ if (line.trim()) logger.log(line);
1336
+ }
1337
+ });
1338
+ child.on("close", (code, sig) => {
1339
+ signal?.removeEventListener("abort", onAbort);
1340
+ if (sig === "SIGTERM" || signal?.aborted) {
1341
+ logger.log("Command cancelled.");
1342
+ } else if (code !== 0) {
1343
+ logger.log(`docker compose exited with code ${code}.`);
1344
+ }
1345
+ resolvePromise();
1346
+ });
1347
+ child.on("error", (err) => {
1348
+ signal?.removeEventListener("abort", onAbort);
1349
+ logger.log(`Failed to run docker compose: ${err.message}`);
1350
+ resolvePromise();
1351
+ });
1352
+ });
1182
1353
  }
1183
-
1184
- // src/controllers/ui/create-application.ui-controller.ts
1185
- async function createApplicationUiController(ctx) {
1186
- if (!await isPlatformInitialized()) {
1187
- ctx.log("Error: Cannot create an application \u2014 no platform initialized in this directory.");
1188
- return;
1354
+ function captureDockerCompose(args2, rootDir) {
1355
+ return new Promise((resolvePromise, reject) => {
1356
+ const child = spawn("docker", ["compose", ...args2], {
1357
+ shell: false,
1358
+ stdio: ["ignore", "pipe", "pipe"],
1359
+ cwd: rootDir
1360
+ });
1361
+ let stdout = "";
1362
+ child.stdout.on("data", (data) => {
1363
+ stdout += data.toString();
1364
+ });
1365
+ child.on("close", (code) => {
1366
+ if (code !== 0) reject(new Error(`docker compose config --services exited with code ${code}`));
1367
+ else resolvePromise(stdout);
1368
+ });
1369
+ child.on("error", (err) => reject(err));
1370
+ });
1371
+ }
1372
+ async function getAppComposePaths(localDir, platformName, manifest) {
1373
+ const results = [];
1374
+ for (const app of manifest.applications) {
1375
+ const prefixedPath = join22(localDir, `${platformName}-${app.name}-docker-compose.yml`);
1376
+ const unprefixedPath = join22(localDir, `${app.name}-docker-compose.yml`);
1377
+ let resolved = null;
1378
+ try {
1379
+ await access5(prefixedPath);
1380
+ resolved = prefixedPath;
1381
+ } catch {
1382
+ try {
1383
+ await access5(unprefixedPath);
1384
+ resolved = unprefixedPath;
1385
+ } catch {
1386
+ }
1387
+ }
1388
+ if (resolved) {
1389
+ results.push({ composePath: resolved, appName: app.name });
1390
+ }
1189
1391
  }
1190
- let organizationName;
1191
- let platformName;
1392
+ return results;
1393
+ }
1394
+ async function buildFullComposeArgs(layout, manifest, logger) {
1395
+ const { coreDirName, localDir } = layout;
1396
+ const platformName = manifest.product.name;
1397
+ const prefixedCoreCompose = join22(localDir, `${coreDirName}-docker-compose.yml`);
1398
+ const unprefixedCoreCompose = join22(localDir, "core-docker-compose.yml");
1399
+ let coreComposePath;
1192
1400
  try {
1193
- const manifest = await readPlatformManifest();
1194
- organizationName = manifest.organizationName;
1195
- platformName = manifest.platformName;
1196
- } catch (err) {
1197
- const message = err instanceof Error ? err.message : String(err);
1198
- ctx.log(`Error: Could not read .platform.json \u2014 ${message}`);
1199
- return;
1200
- }
1201
- const applicationName = await ctx.prompt("Application name:");
1202
- if (!/^[a-z0-9-]+$/.test(applicationName)) {
1203
- ctx.log(`Error: Application name "${applicationName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
1204
- return;
1401
+ await access5(prefixedCoreCompose);
1402
+ coreComposePath = prefixedCoreCompose;
1403
+ } catch {
1404
+ coreComposePath = unprefixedCoreCompose;
1405
+ }
1406
+ const fileArgs = [
1407
+ "-f",
1408
+ join22(localDir, "platform-docker-compose.yml"),
1409
+ "-f",
1410
+ coreComposePath
1411
+ ];
1412
+ const appEntries = await getAppComposePaths(localDir, platformName, manifest);
1413
+ for (const { composePath } of appEntries) {
1414
+ fileArgs.push("-f", composePath);
1415
+ }
1416
+ for (const app of manifest.applications) {
1417
+ if (!appEntries.find((e) => e.appName === app.name)) {
1418
+ logger.log(`Warning: No docker-compose found for application "${app.name}" in ${coreDirName}/local/ \u2014 skipping.`);
1419
+ }
1205
1420
  }
1206
- const applicationDisplayName = await ctx.prompt("Application display name:");
1207
- const applicationDescription = await ctx.prompt("Application description:");
1208
- const hasUserInterfaceAnswer = await ctx.prompt("Does this application have a user interface? (yes/no):");
1209
- if (hasUserInterfaceAnswer !== "yes" && hasUserInterfaceAnswer !== "no") {
1210
- ctx.log(`Error: Please answer "yes" or "no".`);
1211
- return;
1421
+ return fileArgs;
1422
+ }
1423
+ async function buildSelectedComposeFiles(layout, selectedManifest, includeCore) {
1424
+ const { coreDirName, localDir } = layout;
1425
+ const platformName = selectedManifest.product.name;
1426
+ const files = [];
1427
+ if (includeCore) {
1428
+ const prefixedCoreCompose = join22(localDir, `${coreDirName}-docker-compose.yml`);
1429
+ const unprefixedCoreCompose = join22(localDir, "core-docker-compose.yml");
1430
+ let coreComposePath;
1431
+ try {
1432
+ await access5(prefixedCoreCompose);
1433
+ coreComposePath = prefixedCoreCompose;
1434
+ } catch {
1435
+ coreComposePath = unprefixedCoreCompose;
1436
+ }
1437
+ files.push(join22(localDir, "platform-docker-compose.yml"), coreComposePath);
1212
1438
  }
1213
- const hasBackendServiceAnswer = await ctx.prompt("Does this application have a backend service? (yes/no):");
1214
- if (hasBackendServiceAnswer !== "yes" && hasBackendServiceAnswer !== "no") {
1215
- ctx.log(`Error: Please answer "yes" or "no".`);
1216
- return;
1439
+ const appEntries = await getAppComposePaths(localDir, platformName, selectedManifest);
1440
+ for (const { composePath } of appEntries) {
1441
+ files.push(composePath);
1217
1442
  }
1218
- await createApplicationService(
1219
- {
1220
- organizationName,
1221
- platformName,
1222
- applicationName,
1223
- applicationDisplayName,
1224
- applicationDescription,
1225
- hasUserInterface: hasUserInterfaceAnswer === "yes",
1226
- hasBackendService: hasBackendServiceAnswer === "yes"
1227
- },
1228
- ctx
1229
- );
1443
+ return files;
1230
1444
  }
1231
-
1232
- // src/services/init.service.ts
1233
- async function initService(params, logger) {
1234
- await init(params, logger);
1445
+ function extractFilePaths(fileArgs) {
1446
+ const paths = [];
1447
+ for (let i = 0; i < fileArgs.length; i++) {
1448
+ if (fileArgs[i] === "-f" && i + 1 < fileArgs.length) {
1449
+ paths.push(fileArgs[i + 1]);
1450
+ i++;
1451
+ }
1452
+ }
1453
+ return paths;
1454
+ }
1455
+ async function getServicesFromComposeFiles(selectedFiles, allFiles, rootDir) {
1456
+ const selectedSet = new Set(selectedFiles);
1457
+ const contextFiles = allFiles.filter((f) => !selectedSet.has(f));
1458
+ if (contextFiles.length === 0) {
1459
+ const fileArgs = selectedFiles.flatMap((f) => ["-f", f]);
1460
+ const output = await captureDockerCompose([...fileArgs, "config", "--services"], rootDir);
1461
+ return output.split("\n").map((s) => s.trim()).filter(Boolean);
1462
+ }
1463
+ const allFileArgs = allFiles.flatMap((f) => ["-f", f]);
1464
+ const allOutput = await captureDockerCompose([...allFileArgs, "config", "--services"], rootDir);
1465
+ const allServices = new Set(allOutput.split("\n").map((s) => s.trim()).filter(Boolean));
1466
+ const contextFileArgs = contextFiles.flatMap((f) => ["-f", f]);
1467
+ let contextServices;
1468
+ try {
1469
+ const contextOutput = await captureDockerCompose([...contextFileArgs, "config", "--services"], rootDir);
1470
+ contextServices = new Set(contextOutput.split("\n").map((s) => s.trim()).filter(Boolean));
1471
+ } catch {
1472
+ const selectedFileArgs = selectedFiles.flatMap((f) => ["-f", f]);
1473
+ const fallbackOutput = await captureDockerCompose([...selectedFileArgs, "config", "--services"], rootDir);
1474
+ return fallbackOutput.split("\n").map((s) => s.trim()).filter(Boolean);
1475
+ }
1476
+ return [...allServices].filter((s) => !contextServices.has(s));
1477
+ }
1478
+ async function startEnvironment(layout, manifest, logger, signal, includeCore = true, fullManifest) {
1479
+ const { rootDir, localDir } = layout;
1480
+ const platformName = manifest.product.name;
1481
+ const envFile = join22(localDir, ".env");
1482
+ const isSelective = fullManifest !== void 0;
1483
+ const fileArgs = await buildFullComposeArgs(layout, isSelective ? fullManifest : manifest, logger);
1484
+ const projectArgs = ["-p", `${platformName}-platform`, "--project-directory", localDir, "--env-file", envFile];
1485
+ if (isSelective) {
1486
+ const selectedFiles = await buildSelectedComposeFiles(layout, manifest, includeCore);
1487
+ const allFiles = extractFilePaths(fileArgs);
1488
+ const services = await getServicesFromComposeFiles(selectedFiles, allFiles, rootDir);
1489
+ if (services.length === 0) {
1490
+ logger.log("No services found for selected targets. Nothing to start.");
1491
+ return;
1492
+ }
1493
+ logger.log(`Starting: ${services.join(", ")}...`);
1494
+ await runDockerCompose([...projectArgs, ...fileArgs, "up", "-d", "--build", ...services], logger, rootDir, signal);
1495
+ } else {
1496
+ logger.log("Starting environment...");
1497
+ await runDockerCompose([...projectArgs, ...fileArgs, "up", "-d", "--build", "--remove-orphans"], logger, rootDir, signal);
1498
+ }
1499
+ }
1500
+ async function stopEnvironment(layout, manifest, logger, signal, includeCore = true, fullManifest) {
1501
+ const { rootDir, localDir } = layout;
1502
+ const platformName = manifest.product.name;
1503
+ const envFile = join22(localDir, ".env");
1504
+ const isSelective = fullManifest !== void 0;
1505
+ const fileArgs = await buildFullComposeArgs(layout, isSelective ? fullManifest : manifest, logger);
1506
+ const projectArgs = ["-p", `${platformName}-platform`, "--project-directory", localDir, "--env-file", envFile];
1507
+ if (isSelective) {
1508
+ const selectedFiles = await buildSelectedComposeFiles(layout, manifest, includeCore);
1509
+ const allFiles = extractFilePaths(fileArgs);
1510
+ const services = await getServicesFromComposeFiles(selectedFiles, allFiles, rootDir);
1511
+ if (services.length === 0) {
1512
+ logger.log("No services found for selected targets. Nothing to stop.");
1513
+ return;
1514
+ }
1515
+ logger.log(`Stopping: ${services.join(", ")}...`);
1516
+ await runDockerCompose([...projectArgs, ...fileArgs, "stop", ...services], logger, rootDir, signal);
1517
+ } else {
1518
+ logger.log("Stopping environment...");
1519
+ await runDockerCompose([...projectArgs, ...fileArgs, "down"], logger, rootDir, signal);
1520
+ }
1521
+ }
1522
+ async function destroyEnvironment(layout, manifest, logger, signal, includeCore = true, fullManifest) {
1523
+ const { rootDir, localDir } = layout;
1524
+ const platformName = manifest.product.name;
1525
+ const envFile = join22(localDir, ".env");
1526
+ const isSelective = fullManifest !== void 0;
1527
+ const fileArgs = await buildFullComposeArgs(layout, isSelective ? fullManifest : manifest, logger);
1528
+ const projectArgs = ["-p", `${platformName}-platform`, "--project-directory", localDir, "--env-file", envFile];
1529
+ if (isSelective) {
1530
+ const selectedFiles = await buildSelectedComposeFiles(layout, manifest, includeCore);
1531
+ const allFiles = extractFilePaths(fileArgs);
1532
+ const services = await getServicesFromComposeFiles(selectedFiles, allFiles, rootDir);
1533
+ if (services.length === 0) {
1534
+ logger.log("No services found for selected targets. Nothing to destroy.");
1535
+ return;
1536
+ }
1537
+ logger.log(`Destroying: ${services.join(", ")}...`);
1538
+ await runDockerCompose([...projectArgs, ...fileArgs, "stop", ...services], logger, rootDir, signal);
1539
+ if (!signal?.aborted) {
1540
+ await runDockerCompose([...projectArgs, ...fileArgs, "rm", "-f", ...services], logger, rootDir, signal);
1541
+ }
1542
+ } else {
1543
+ logger.log("Destroying environment...");
1544
+ await runDockerCompose([...projectArgs, ...fileArgs, "down", "-v", "--rmi", "all"], logger, rootDir, signal);
1545
+ }
1235
1546
  }
1236
1547
 
1237
- // src/controllers/ui/init.ui-controller.ts
1238
- async function initUiController(ctx) {
1239
- if (await isPlatformInitialized()) {
1240
- ctx.log("Error: Cannot initialize a new platform \u2014 a platform is already initialized in this directory.");
1241
- return;
1242
- }
1243
- const organizationName = await ctx.prompt("Organization name:");
1548
+ // src/commands/local-scripts/npm-orchestrator.ts
1549
+ import { spawn as spawn2 } from "child_process";
1550
+ import { access as access6 } from "fs/promises";
1551
+ import { resolve as resolve4, join as join23 } from "path";
1552
+ function runCommand(command, args2, workDir, logger, signal) {
1553
+ return new Promise((resolvePromise) => {
1554
+ const child = spawn2(command, args2, {
1555
+ cwd: workDir,
1556
+ shell: false,
1557
+ stdio: ["ignore", "pipe", "pipe"]
1558
+ });
1559
+ const onAbort = () => {
1560
+ child.kill("SIGTERM");
1561
+ };
1562
+ if (signal) {
1563
+ if (signal.aborted) {
1564
+ child.kill("SIGTERM");
1565
+ resolvePromise();
1566
+ return;
1567
+ }
1568
+ signal.addEventListener("abort", onAbort, { once: true });
1569
+ }
1570
+ child.stdout.on("data", (data) => {
1571
+ for (const line of data.toString().split("\n")) {
1572
+ if (line.trim()) logger.log(line);
1573
+ }
1574
+ });
1575
+ child.stderr.on("data", (data) => {
1576
+ for (const line of data.toString().split("\n")) {
1577
+ if (line.trim()) logger.log(line);
1578
+ }
1579
+ });
1580
+ child.on("close", (code, sig) => {
1581
+ signal?.removeEventListener("abort", onAbort);
1582
+ if (sig === "SIGTERM" || signal?.aborted) {
1583
+ logger.log("Command cancelled.");
1584
+ } else if (code !== 0) {
1585
+ logger.log(`Command "${command} ${args2.join(" ")}" exited with code ${code}.`);
1586
+ }
1587
+ resolvePromise();
1588
+ });
1589
+ child.on("error", (err) => {
1590
+ signal?.removeEventListener("abort", onAbort);
1591
+ logger.log(`Failed to run "${command} ${args2.join(" ")}": ${err.message}`);
1592
+ resolvePromise();
1593
+ });
1594
+ });
1595
+ }
1596
+ async function dirExists(dirPath) {
1597
+ try {
1598
+ await access6(dirPath);
1599
+ return true;
1600
+ } catch {
1601
+ return false;
1602
+ }
1603
+ }
1604
+ async function installDependencies(layout, manifest, logger, signal, includeCore = true) {
1605
+ const { coreDir, coreDirName } = layout;
1606
+ const appDirs = [];
1607
+ for (const app of manifest.applications) {
1608
+ const appDir = resolve4(join23(coreDir), app.localPath);
1609
+ if (!await dirExists(appDir)) {
1610
+ logger.log(`Warning: Application directory "${app.name}" not found at ${appDir} \u2014 skipping install.`);
1611
+ continue;
1612
+ }
1613
+ appDirs.push({ name: app.name, dir: appDir });
1614
+ }
1615
+ const targets = includeCore ? [{ name: coreDirName, dir: coreDir }, ...appDirs] : appDirs;
1616
+ logger.log(`Installing dependencies in parallel: ${targets.map((t) => t.name).join(", ")}...`);
1617
+ await Promise.all(
1618
+ targets.map(
1619
+ ({ name, dir }) => runCommand("npm", ["install"], dir, logger, signal).then(() => {
1620
+ logger.log(`\u2713 ${name} install done`);
1621
+ })
1622
+ )
1623
+ );
1624
+ }
1625
+ async function buildAll(layout, manifest, logger, signal, includeCore = true) {
1626
+ const { coreDir, coreDirName } = layout;
1627
+ if (includeCore) {
1628
+ logger.log(`Building ${coreDirName}/...`);
1629
+ await runCommand("npx", ["turbo", "build"], coreDir, logger, signal);
1630
+ if (signal?.aborted) return;
1631
+ }
1632
+ const appDirs = [];
1633
+ for (const app of manifest.applications) {
1634
+ const appDir = resolve4(join23(coreDir), app.localPath);
1635
+ if (!await dirExists(appDir)) {
1636
+ logger.log(`Warning: Application directory "${app.name}" not found at ${appDir} \u2014 skipping build.`);
1637
+ continue;
1638
+ }
1639
+ appDirs.push({ name: app.name, dir: appDir });
1640
+ }
1641
+ if (appDirs.length === 0) return;
1642
+ logger.log(`Building apps in parallel: ${appDirs.map((a) => a.name).join(", ")}...`);
1643
+ await Promise.all(
1644
+ appDirs.map(
1645
+ ({ name, dir }) => runCommand("npm", ["run", "build"], dir, logger, signal).then(() => {
1646
+ logger.log(`\u2713 ${name} build done`);
1647
+ })
1648
+ )
1649
+ );
1650
+ }
1651
+
1652
+ // src/commands/local-scripts/local-script.command.ts
1653
+ var INSTALL_COMMAND_NAME = "install";
1654
+ var BUILD_COMMAND_NAME = "build";
1655
+ var START_COMMAND_NAME = "start";
1656
+ var STOP_COMMAND_NAME = "stop";
1657
+ var DESTROY_COMMAND_NAME = "destroy";
1658
+ var CORE_APP_NAME = "core";
1659
+ var installCommand = {
1660
+ name: INSTALL_COMMAND_NAME,
1661
+ description: "Install dependencies in the local dev environment",
1662
+ hidden: (ctx) => !ctx.platformInitialized
1663
+ };
1664
+ var buildCommand = {
1665
+ name: BUILD_COMMAND_NAME,
1666
+ description: "Build the local dev environment",
1667
+ hidden: (ctx) => !ctx.platformInitialized
1668
+ };
1669
+ var startCommand = {
1670
+ name: START_COMMAND_NAME,
1671
+ description: "Start the local dev environment",
1672
+ hidden: (ctx) => !ctx.platformInitialized
1673
+ };
1674
+ var stopCommand = {
1675
+ name: STOP_COMMAND_NAME,
1676
+ description: "Stop the local dev environment",
1677
+ hidden: (ctx) => !ctx.platformInitialized
1678
+ };
1679
+ var destroyCommand = {
1680
+ name: DESTROY_COMMAND_NAME,
1681
+ description: "Destroy the local dev environment",
1682
+ hidden: (ctx) => !ctx.platformInitialized
1683
+ };
1684
+ var localScriptCommands = [
1685
+ installCommand,
1686
+ buildCommand,
1687
+ startCommand,
1688
+ stopCommand,
1689
+ destroyCommand
1690
+ ];
1691
+ async function runLocalScript(scriptName, logger, signal, appNames) {
1692
+ const layout = await findPlatformLayout();
1693
+ if (!layout) {
1694
+ logger.log(`Error: Cannot run "${scriptName}" \u2014 no platform initialized in this directory.`);
1695
+ return;
1696
+ }
1697
+ const { rootDir, coreDirName } = layout;
1698
+ let manifest;
1699
+ try {
1700
+ manifest = await readManifest(rootDir, coreDirName);
1701
+ } catch (err) {
1702
+ logger.log(`Error: Could not read product manifest \u2014 ${formatError(err)}`);
1703
+ return;
1704
+ }
1705
+ let includeCore = true;
1706
+ const fullManifest = manifest;
1707
+ let isSelective = false;
1708
+ if (appNames && appNames.length > 0) {
1709
+ isSelective = true;
1710
+ includeCore = appNames.includes(CORE_APP_NAME);
1711
+ const appNamesWithoutCore = appNames.filter((n) => n !== CORE_APP_NAME);
1712
+ const nameSet = new Set(appNamesWithoutCore);
1713
+ const unknown = appNamesWithoutCore.filter((n) => !manifest.applications.some((a) => a.name === n));
1714
+ if (unknown.length > 0) {
1715
+ logger.log(`Warning: Unknown application(s): ${unknown.join(", ")} \u2014 ignoring.`);
1716
+ }
1717
+ const filtered = manifest.applications.filter((a) => nameSet.has(a.name));
1718
+ if (!includeCore && filtered.length === 0) {
1719
+ logger.log("No matching applications found. Nothing to do.");
1720
+ return;
1721
+ }
1722
+ manifest = { ...manifest, applications: filtered };
1723
+ }
1724
+ switch (scriptName) {
1725
+ case START_COMMAND_NAME:
1726
+ await startEnvironment(layout, manifest, logger, signal, includeCore, isSelective ? fullManifest : void 0);
1727
+ break;
1728
+ case STOP_COMMAND_NAME:
1729
+ await stopEnvironment(layout, manifest, logger, signal, includeCore, isSelective ? fullManifest : void 0);
1730
+ break;
1731
+ case DESTROY_COMMAND_NAME:
1732
+ await destroyEnvironment(layout, manifest, logger, signal, includeCore, isSelective ? fullManifest : void 0);
1733
+ break;
1734
+ case INSTALL_COMMAND_NAME:
1735
+ await installDependencies(layout, manifest, logger, signal, includeCore);
1736
+ break;
1737
+ case BUILD_COMMAND_NAME:
1738
+ await buildAll(layout, manifest, logger, signal, includeCore);
1739
+ break;
1740
+ default:
1741
+ logger.log(`Error: Unknown script "${scriptName}".`);
1742
+ }
1743
+ }
1744
+
1745
+ // src/commands/status/status-checks.ts
1746
+ function spawnCapture(cmd, args2, cwd5) {
1747
+ return new Promise((resolvePromise) => {
1748
+ const child = spawn3(cmd, args2, {
1749
+ cwd: cwd5,
1750
+ shell: false,
1751
+ stdio: ["ignore", "pipe", "pipe"]
1752
+ });
1753
+ let stdout = "";
1754
+ let stderr = "";
1755
+ child.stdout.on("data", (data) => {
1756
+ stdout += data.toString();
1757
+ });
1758
+ child.stderr.on("data", (data) => {
1759
+ stderr += data.toString();
1760
+ });
1761
+ child.on("close", (code) => {
1762
+ resolvePromise({ code: code ?? 1, stdout, stderr });
1763
+ });
1764
+ child.on("error", () => {
1765
+ resolvePromise({ code: 1, stdout, stderr });
1766
+ });
1767
+ });
1768
+ }
1769
+ async function pathExists(p) {
1770
+ try {
1771
+ await access7(p);
1772
+ return true;
1773
+ } catch {
1774
+ return false;
1775
+ }
1776
+ }
1777
+ async function checkNodeVersion() {
1778
+ const { code, stdout } = await spawnCapture("node", ["--version"]);
1779
+ if (code !== 0 || !stdout.trim()) {
1780
+ return { name: "Node.js", available: false, version: null, versionOk: false, detail: "not found in PATH" };
1781
+ }
1782
+ const version2 = stdout.trim();
1783
+ const match = version2.match(/^v(\d+)/);
1784
+ const major = match ? parseInt(match[1], 10) : 0;
1785
+ const versionOk = major >= 22;
1786
+ return {
1787
+ name: "Node.js",
1788
+ available: true,
1789
+ version: version2,
1790
+ versionOk,
1791
+ detail: versionOk ? void 0 : `>=22 required, found ${version2}`
1792
+ };
1793
+ }
1794
+ async function checkDocker() {
1795
+ const { code, stdout } = await spawnCapture("docker", ["info", "--format", "{{.ServerVersion}}"]);
1796
+ if (code === 0 && stdout.trim()) {
1797
+ return { name: "Docker", available: true, version: stdout.trim(), versionOk: true };
1798
+ }
1799
+ const { code: vCode, stdout: vOut } = await spawnCapture("docker", ["--version"]);
1800
+ if (vCode === 0 && vOut.trim()) {
1801
+ return {
1802
+ name: "Docker",
1803
+ available: false,
1804
+ version: null,
1805
+ versionOk: false,
1806
+ detail: "installed but daemon is not running"
1807
+ };
1808
+ }
1809
+ return { name: "Docker", available: false, version: null, versionOk: false, detail: "not found in PATH" };
1810
+ }
1811
+ async function checkInstalled(layout, manifest) {
1812
+ const results = [];
1813
+ const coreCheck = {
1814
+ name: layout.coreDirName,
1815
+ path: join24(layout.coreDir, "node_modules"),
1816
+ exists: await pathExists(join24(layout.coreDir, "node_modules"))
1817
+ };
1818
+ results.push(coreCheck);
1819
+ for (const app of manifest.applications) {
1820
+ const appDir = resolve5(layout.coreDir, app.localPath);
1821
+ const nodeModulesPath = join24(appDir, "node_modules");
1822
+ results.push({
1823
+ name: app.name,
1824
+ path: nodeModulesPath,
1825
+ exists: await pathExists(nodeModulesPath)
1826
+ });
1827
+ }
1828
+ return results;
1829
+ }
1830
+ async function checkBuilt(layout, manifest) {
1831
+ const results = [];
1832
+ const coreTurboPath = join24(layout.coreDir, ".turbo");
1833
+ results.push({
1834
+ name: layout.coreDirName,
1835
+ path: coreTurboPath,
1836
+ exists: await pathExists(coreTurboPath)
1837
+ });
1838
+ for (const app of manifest.applications) {
1839
+ const appDir = resolve5(layout.coreDir, app.localPath);
1840
+ const appTurboPath = join24(appDir, ".turbo");
1841
+ results.push({
1842
+ name: app.name,
1843
+ path: appTurboPath,
1844
+ exists: await pathExists(appTurboPath)
1845
+ });
1846
+ }
1847
+ return results;
1848
+ }
1849
+ async function resolveComposeFiles(layout, manifest) {
1850
+ const { localDir, coreDirName } = layout;
1851
+ const platformName = manifest.product.name;
1852
+ const files = [join24(localDir, "platform-docker-compose.yml")];
1853
+ const prefixedCore = join24(localDir, `${coreDirName}-docker-compose.yml`);
1854
+ const unprefixedCore = join24(localDir, "core-docker-compose.yml");
1855
+ files.push(await pathExists(prefixedCore) ? prefixedCore : unprefixedCore);
1856
+ for (const app of manifest.applications) {
1857
+ const prefixed = join24(localDir, `${platformName}-${app.name}-docker-compose.yml`);
1858
+ const unprefixed = join24(localDir, `${app.name}-docker-compose.yml`);
1859
+ if (await pathExists(prefixed)) files.push(prefixed);
1860
+ else if (await pathExists(unprefixed)) files.push(unprefixed);
1861
+ }
1862
+ return files;
1863
+ }
1864
+ async function checkContainers(layout, manifest) {
1865
+ const platformName = manifest.product.name;
1866
+ const projectName = `${platformName}-platform`;
1867
+ const composeFiles = await resolveComposeFiles(layout, manifest);
1868
+ const fileArgs = composeFiles.flatMap((f) => ["-f", f]);
1869
+ const { code: cfgCode, stdout: cfgOut } = await spawnCapture(
1870
+ "docker",
1871
+ ["compose", "-p", projectName, ...fileArgs, "config", "--services"],
1872
+ layout.rootDir
1873
+ );
1874
+ const expectedServices = cfgCode === 0 ? cfgOut.trim().split("\n").map((s) => s.trim()).filter(Boolean) : [];
1875
+ const { stdout: psOut } = await spawnCapture(
1876
+ "docker",
1877
+ ["compose", "-p", projectName, ...fileArgs, "ps", "--format", "json"],
1878
+ layout.rootDir
1879
+ );
1880
+ const runningByService = /* @__PURE__ */ new Map();
1881
+ for (const line of psOut.trim().split("\n")) {
1882
+ const trimmed = line.trim();
1883
+ if (!trimmed) continue;
1884
+ try {
1885
+ const obj = JSON.parse(trimmed);
1886
+ const service = String(obj["Service"] ?? obj["Name"] ?? "");
1887
+ if (service) {
1888
+ runningByService.set(service, {
1889
+ state: String(obj["State"] ?? "unknown").toLowerCase(),
1890
+ ports: String(obj["Publishers"] ? formatPublishers(obj["Publishers"]) : obj["Ports"] ?? "")
1891
+ });
1892
+ }
1893
+ } catch {
1894
+ }
1895
+ }
1896
+ if (expectedServices.length > 0) {
1897
+ return expectedServices.map((service) => {
1898
+ const actual = runningByService.get(service);
1899
+ return {
1900
+ name: service,
1901
+ state: actual ? actual.state : "not started",
1902
+ ports: actual ? actual.ports : ""
1903
+ };
1904
+ });
1905
+ }
1906
+ return Array.from(runningByService.entries()).map(([name, info]) => ({
1907
+ name,
1908
+ ...info
1909
+ }));
1910
+ }
1911
+ function formatPublishers(publishers) {
1912
+ if (!Array.isArray(publishers)) return "";
1913
+ return publishers.map((p) => {
1914
+ if (typeof p !== "object" || p === null) return "";
1915
+ const pub = p;
1916
+ const host = pub["URL"] ?? "0.0.0.0";
1917
+ const published = pub["PublishedPort"];
1918
+ const target = pub["TargetPort"];
1919
+ const proto = pub["Protocol"] ?? "tcp";
1920
+ if (!published || !target) return "";
1921
+ return `${host}:${published}->${target}/${proto}`;
1922
+ }).filter(Boolean).join(", ");
1923
+ }
1924
+ function computeNextStepInfo(result) {
1925
+ if (!result.lifecycle.initialized) {
1926
+ return { step: "init", appNames: [], allApps: true };
1927
+ }
1928
+ if (!result.lifecycle.installed) {
1929
+ const failing = result.lifecycle.installedDetails.filter((d) => !d.exists).map((d) => d.name === result.coreDirName ? CORE_APP_NAME : d.name);
1930
+ const allApps = failing.length === result.lifecycle.installedDetails.length;
1931
+ return { step: "install", appNames: failing, allApps };
1932
+ }
1933
+ if (!result.lifecycle.built) {
1934
+ const failing = result.lifecycle.builtDetails.filter((d) => !d.exists).map((d) => d.name === result.coreDirName ? CORE_APP_NAME : d.name);
1935
+ const allApps = failing.length === result.lifecycle.builtDetails.length;
1936
+ return { step: "build", appNames: failing, allApps };
1937
+ }
1938
+ if (!result.lifecycle.running) {
1939
+ const failing = result.containersByApp.filter((g) => !g.allRunning).map((g) => g.appName);
1940
+ const allApps = failing.length === result.containersByApp.length;
1941
+ return { step: "start", appNames: failing, allApps };
1942
+ }
1943
+ return { step: null, appNames: [], allApps: true };
1944
+ }
1945
+ async function getServicesForComposeFiles(selectedFiles, allFiles, rootDir) {
1946
+ const selectedSet = new Set(selectedFiles);
1947
+ const contextFiles = allFiles.filter((f) => !selectedSet.has(f));
1948
+ if (contextFiles.length === 0) {
1949
+ const fileArgs = selectedFiles.flatMap((f) => ["-f", f]);
1950
+ const { stdout } = await spawnCapture("docker", ["compose", ...fileArgs, "config", "--services"], rootDir);
1951
+ return stdout.split("\n").map((s) => s.trim()).filter(Boolean);
1952
+ }
1953
+ const allFileArgs = allFiles.flatMap((f) => ["-f", f]);
1954
+ const { stdout: allOut } = await spawnCapture("docker", ["compose", ...allFileArgs, "config", "--services"], rootDir);
1955
+ const allServices = new Set(allOut.split("\n").map((s) => s.trim()).filter(Boolean));
1956
+ const contextFileArgs = contextFiles.flatMap((f) => ["-f", f]);
1957
+ const { stdout: ctxOut } = await spawnCapture("docker", ["compose", ...contextFileArgs, "config", "--services"], rootDir);
1958
+ const contextServices = new Set(ctxOut.split("\n").map((s) => s.trim()).filter(Boolean));
1959
+ return [...allServices].filter((s) => !contextServices.has(s));
1960
+ }
1961
+ async function checkContainersPerApp(layout, manifest, allContainers) {
1962
+ const { localDir, coreDirName, rootDir } = layout;
1963
+ const platformName = manifest.product.name;
1964
+ const prefixedCore = join24(localDir, `${coreDirName}-docker-compose.yml`);
1965
+ const unprefixedCore = join24(localDir, "core-docker-compose.yml");
1966
+ const coreComposePath = await pathExists(prefixedCore) ? prefixedCore : unprefixedCore;
1967
+ const platformComposePath = join24(localDir, "platform-docker-compose.yml");
1968
+ const allFiles = [platformComposePath, coreComposePath];
1969
+ const appComposeMap = /* @__PURE__ */ new Map();
1970
+ for (const app of manifest.applications) {
1971
+ const prefixed = join24(localDir, `${platformName}-${app.name}-docker-compose.yml`);
1972
+ const unprefixed = join24(localDir, `${app.name}-docker-compose.yml`);
1973
+ if (await pathExists(prefixed)) {
1974
+ appComposeMap.set(app.name, prefixed);
1975
+ allFiles.push(prefixed);
1976
+ } else if (await pathExists(unprefixed)) {
1977
+ appComposeMap.set(app.name, unprefixed);
1978
+ allFiles.push(unprefixed);
1979
+ }
1980
+ }
1981
+ const containerByName = new Map(allContainers.map((c) => [c.name, c]));
1982
+ const coreFiles = [platformComposePath, coreComposePath];
1983
+ const { stdout: coreOut } = await spawnCapture(
1984
+ "docker",
1985
+ ["compose", ...coreFiles.flatMap((f) => ["-f", f]), "config", "--services"],
1986
+ rootDir
1987
+ );
1988
+ const coreServiceNames = coreOut.split("\n").map((s) => s.trim()).filter(Boolean);
1989
+ const appServicesMap = /* @__PURE__ */ new Map();
1990
+ for (const app of manifest.applications) {
1991
+ const appComposePath = appComposeMap.get(app.name);
1992
+ if (!appComposePath) continue;
1993
+ try {
1994
+ const appServices = await getServicesForComposeFiles([appComposePath], allFiles, rootDir);
1995
+ appServicesMap.set(app.name, appServices);
1996
+ } catch {
1997
+ }
1998
+ }
1999
+ const groups = [];
2000
+ const coreContainers = coreServiceNames.map((s) => containerByName.get(s) ?? { name: s, state: "not started", ports: "" });
2001
+ groups.push({
2002
+ appName: CORE_APP_NAME,
2003
+ containers: coreContainers,
2004
+ allRunning: coreContainers.length > 0 && coreContainers.every((c) => c.state === "running")
2005
+ });
2006
+ for (const app of manifest.applications) {
2007
+ const appServices = appServicesMap.get(app.name);
2008
+ if (!appServices) continue;
2009
+ const appContainers = appServices.map((s) => containerByName.get(s) ?? { name: s, state: "not started", ports: "" });
2010
+ groups.push({
2011
+ appName: app.name,
2012
+ containers: appContainers,
2013
+ allRunning: appContainers.length > 0 && appContainers.every((c) => c.state === "running")
2014
+ });
2015
+ }
2016
+ return groups;
2017
+ }
2018
+ async function gatherStatus() {
2019
+ const layout = await findPlatformLayout();
2020
+ const [nodeCheck, dockerCheck] = await Promise.all([checkNodeVersion(), checkDocker()]);
2021
+ const prerequisites = [nodeCheck, dockerCheck];
2022
+ if (!layout) {
2023
+ return {
2024
+ projectInfo: null,
2025
+ prerequisites,
2026
+ lifecycle: {
2027
+ initialized: false,
2028
+ installed: false,
2029
+ installedDetails: [],
2030
+ built: false,
2031
+ builtDetails: [],
2032
+ running: false
2033
+ },
2034
+ containers: [],
2035
+ containersByApp: [],
2036
+ coreDirName: null
2037
+ };
2038
+ }
2039
+ let manifest;
2040
+ try {
2041
+ manifest = await readManifest(layout.rootDir, layout.coreDirName);
2042
+ } catch {
2043
+ return {
2044
+ projectInfo: null,
2045
+ prerequisites,
2046
+ lifecycle: {
2047
+ initialized: true,
2048
+ installed: false,
2049
+ installedDetails: [],
2050
+ built: false,
2051
+ builtDetails: [],
2052
+ running: false
2053
+ },
2054
+ containers: [],
2055
+ containersByApp: [],
2056
+ coreDirName: layout.coreDirName
2057
+ };
2058
+ }
2059
+ const [installedDetails, builtDetails, containers] = await Promise.all([
2060
+ checkInstalled(layout, manifest),
2061
+ checkBuilt(layout, manifest),
2062
+ checkContainers(layout, manifest)
2063
+ ]);
2064
+ const containersByApp = await checkContainersPerApp(layout, manifest, containers);
2065
+ const projectInfo = {
2066
+ productName: manifest.product.name,
2067
+ displayName: manifest.product.displayName,
2068
+ organization: manifest.product.organization,
2069
+ scope: manifest.product.scope,
2070
+ applicationCount: manifest.applications.length,
2071
+ applicationNames: manifest.applications.map((a) => a.name)
2072
+ };
2073
+ return {
2074
+ projectInfo,
2075
+ prerequisites,
2076
+ lifecycle: {
2077
+ initialized: true,
2078
+ installed: installedDetails.every((d) => d.exists),
2079
+ installedDetails,
2080
+ built: builtDetails.every((d) => d.exists),
2081
+ builtDetails,
2082
+ running: containers.length > 0 && containers.every((c) => c.state === "running")
2083
+ },
2084
+ containers,
2085
+ containersByApp,
2086
+ coreDirName: layout.coreDirName
2087
+ };
2088
+ }
2089
+
2090
+ // src/commands/status/status.command.ts
2091
+ var STATUS_COMMAND_NAME = "status";
2092
+ var statusCommand = {
2093
+ name: STATUS_COMMAND_NAME,
2094
+ description: "Show platform status and health checks",
2095
+ hidden: (ctx) => !ctx.platformInitialized
2096
+ };
2097
+
2098
+ // src/commands/manage-platform-admins/manage-platform-admins.command.ts
2099
+ import { join as join25 } from "path";
2100
+ import { fetch as undiciFetch2, Agent as Agent2 } from "undici";
2101
+ var MANAGE_PLATFORM_ADMINS_COMMAND_NAME = "manage-platform-admins";
2102
+ var managePlatformAdminsCommand = {
2103
+ name: MANAGE_PLATFORM_ADMINS_COMMAND_NAME,
2104
+ description: "Manage platform administrators (list, add, remove)",
2105
+ hidden: (ctx) => !ctx.platformInitialized
2106
+ };
2107
+ async function getGatewayConfig(logger) {
2108
+ const layout = await findPlatformLayout();
2109
+ if (!layout) {
2110
+ logger.log("Error: Cannot manage platform admins \u2014 no platform initialized in this directory.");
2111
+ return null;
2112
+ }
2113
+ const envPath = join25(layout.localDir, ".env");
2114
+ let env;
2115
+ try {
2116
+ env = await readEnvFile(envPath);
2117
+ } catch (error) {
2118
+ logger.log(`Error: ${error.message}`);
2119
+ return null;
2120
+ }
2121
+ const gatewayUrl = env.get("PAE_GATEWAY_HOST_URL");
2122
+ const accessSecret = env.get("PAE_GATEWAY_SERVICE_ACCESS_SECRET");
2123
+ if (!gatewayUrl) {
2124
+ logger.log("Error: PAE_GATEWAY_HOST_URL is not set in core/local/.env");
2125
+ return null;
2126
+ }
2127
+ if (!accessSecret) {
2128
+ logger.log("Error: PAE_GATEWAY_SERVICE_ACCESS_SECRET is not set in core/local/.env");
2129
+ return null;
2130
+ }
2131
+ return { gatewayUrl, accessSecret };
2132
+ }
2133
+ async function listPlatformAdmins(logger) {
2134
+ const config = await getGatewayConfig(logger);
2135
+ if (!config) return [];
2136
+ const { gatewayUrl, accessSecret } = config;
2137
+ const agent = new Agent2({ connect: { rejectUnauthorized: false } });
2138
+ let response;
2139
+ try {
2140
+ response = await undiciFetch2(`${gatewayUrl}/applications/platform/rules`, {
2141
+ method: "GET",
2142
+ headers: {
2143
+ Accept: "application/json",
2144
+ Authorization: `Bearer ${accessSecret}`
2145
+ },
2146
+ dispatcher: agent
2147
+ });
2148
+ } catch (error) {
2149
+ const err = error;
2150
+ const cause = err.cause instanceof Error ? ` (cause: ${err.cause.message})` : err.cause ? ` (cause: ${String(err.cause)})` : "";
2151
+ logger.log(`Error: Failed to reach gateway \u2014 ${err.message}${cause}`);
2152
+ return [];
2153
+ }
2154
+ if (!response.ok) {
2155
+ const body = await response.text().catch(() => "");
2156
+ logger.log(`Error: Server responded with ${response.status}${body ? ` \u2014 ${body}` : ""}`);
2157
+ return [];
2158
+ }
2159
+ const rules = await response.json();
2160
+ return rules.filter(
2161
+ (r) => r.subjectType === "user" && r.resourceType === "role" && r.resourceName === "admin"
2162
+ ).map((r) => ({ username: r.subject, ruleId: String(r.id) }));
2163
+ }
2164
+ async function addPlatformAdmin(username, logger) {
2165
+ const config = await getGatewayConfig(logger);
2166
+ if (!config) return false;
2167
+ const { gatewayUrl, accessSecret } = config;
2168
+ const agent = new Agent2({ connect: { rejectUnauthorized: false } });
2169
+ let response;
2170
+ try {
2171
+ response = await undiciFetch2(`${gatewayUrl}/applications/platform/rules`, {
2172
+ method: "POST",
2173
+ headers: {
2174
+ "Content-Type": "application/json",
2175
+ Accept: "application/json",
2176
+ Authorization: `Bearer ${accessSecret}`
2177
+ },
2178
+ body: JSON.stringify({
2179
+ subject: username,
2180
+ subjectType: "user",
2181
+ resourceType: "role",
2182
+ resource: "admin"
2183
+ }),
2184
+ dispatcher: agent
2185
+ });
2186
+ } catch (error) {
2187
+ const err = error;
2188
+ const cause = err.cause instanceof Error ? ` (cause: ${err.cause.message})` : err.cause ? ` (cause: ${String(err.cause)})` : "";
2189
+ logger.log(`Error: Failed to reach gateway \u2014 ${err.message}${cause}`);
2190
+ return false;
2191
+ }
2192
+ if (response.status === 409) {
2193
+ logger.log(`'${username}' is already a platform admin.`);
2194
+ return false;
2195
+ }
2196
+ if (!response.ok) {
2197
+ const body = await response.text().catch(() => "");
2198
+ logger.log(`Error: Server responded with ${response.status}${body ? ` \u2014 ${body}` : ""}`);
2199
+ return false;
2200
+ }
2201
+ return true;
2202
+ }
2203
+ async function removePlatformAdmin(ruleId, logger) {
2204
+ const config = await getGatewayConfig(logger);
2205
+ if (!config) return false;
2206
+ const { gatewayUrl, accessSecret } = config;
2207
+ const agent = new Agent2({ connect: { rejectUnauthorized: false } });
2208
+ let response;
2209
+ try {
2210
+ response = await undiciFetch2(`${gatewayUrl}/applications/platform/rules/${ruleId}`, {
2211
+ method: "DELETE",
2212
+ headers: {
2213
+ Authorization: `Bearer ${accessSecret}`
2214
+ },
2215
+ dispatcher: agent
2216
+ });
2217
+ } catch (error) {
2218
+ const err = error;
2219
+ const cause = err.cause instanceof Error ? ` (cause: ${err.cause.message})` : err.cause ? ` (cause: ${String(err.cause)})` : "";
2220
+ logger.log(`Error: Failed to reach gateway \u2014 ${err.message}${cause}`);
2221
+ return false;
2222
+ }
2223
+ if (!response.ok) {
2224
+ const body = await response.text().catch(() => "");
2225
+ logger.log(`Error: Server responded with ${response.status}${body ? ` \u2014 ${body}` : ""}`);
2226
+ return false;
2227
+ }
2228
+ return true;
2229
+ }
2230
+
2231
+ // src/commands/registry.ts
2232
+ var CommandRegistry = class {
2233
+ commands = /* @__PURE__ */ new Map();
2234
+ register(command) {
2235
+ this.commands.set(command.name, command);
2236
+ }
2237
+ get(name) {
2238
+ return this.commands.get(name);
2239
+ }
2240
+ getAll() {
2241
+ return Array.from(this.commands.values());
2242
+ }
2243
+ search(query) {
2244
+ const all = this.getAll();
2245
+ if (!query) return all;
2246
+ const fzf = new Fzf(all, {
2247
+ selector: (cmd) => `${cmd.name} ${cmd.description}`
2248
+ });
2249
+ return fzf.find(query).map((result) => result.item);
2250
+ }
2251
+ searchVisible(query, visibilityCtx) {
2252
+ const visible = this.getAll().filter((cmd) => !cmd.hidden?.(visibilityCtx));
2253
+ if (!query) return visible;
2254
+ const fzf = new Fzf(visible, {
2255
+ selector: (cmd) => `${cmd.name} ${cmd.description}`
2256
+ });
2257
+ return fzf.find(query).map((result) => result.item);
2258
+ }
2259
+ };
2260
+ var registry = new CommandRegistry();
2261
+ registry.register(initCommand);
2262
+ registry.register(configureIdpCommand);
2263
+ registry.register(createApplicationCommand);
2264
+ registry.register(createServiceModuleCommand);
2265
+ registry.register(createUiModuleCommand);
2266
+ for (const cmd of localScriptCommands) {
2267
+ registry.register(cmd);
2268
+ }
2269
+ registry.register(statusCommand);
2270
+ registry.register(managePlatformAdminsCommand);
2271
+
2272
+ // src/app-state.ts
2273
+ var APP_STATE = {
2274
+ IDLE: "idle",
2275
+ PALETTE: "palette",
2276
+ EXECUTING: "executing",
2277
+ PROMPTING: "prompting"
2278
+ };
2279
+
2280
+ // src/hooks/use-command-runner.ts
2281
+ import { useState as useState2, useCallback, useRef } from "react";
2282
+
2283
+ // src/services/create-application.service.ts
2284
+ async function createApplicationService(params, logger) {
2285
+ await createApplication(params, logger);
2286
+ }
2287
+
2288
+ // src/controllers/ui/create-application.ui-controller.ts
2289
+ async function createApplicationUiController(ctx) {
2290
+ if (!await isPlatformInitialized()) {
2291
+ ctx.log("Error: Cannot create an application \u2014 no platform initialized in this directory.");
2292
+ return;
2293
+ }
2294
+ const applicationName = await ctx.prompt("Application name:");
2295
+ if (!/^[a-z0-9-]+$/.test(applicationName)) {
2296
+ ctx.log(`Error: Application name "${applicationName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
2297
+ return;
2298
+ }
2299
+ const applicationDisplayName = await ctx.prompt("Application display name:");
2300
+ const applicationDescription = await ctx.prompt("Application description:");
2301
+ const hasUserInterface = await ctx.confirm("Does this application have a user interface?");
2302
+ const hasBackendService = await ctx.confirm("Does this application have a backend service?");
2303
+ await createApplicationService(
2304
+ {
2305
+ applicationName,
2306
+ applicationDisplayName,
2307
+ applicationDescription,
2308
+ hasUserInterface,
2309
+ hasBackendService
2310
+ },
2311
+ ctx
2312
+ );
2313
+ }
2314
+
2315
+ // src/services/init.service.ts
2316
+ async function initService(params, logger) {
2317
+ await init(params, logger);
2318
+ }
2319
+
2320
+ // src/controllers/ui/init.ui-controller.ts
2321
+ async function initUiController(ctx) {
2322
+ if (await isPlatformInitialized()) {
2323
+ ctx.log("Error: Cannot initialize a new platform \u2014 a platform is already initialized in this directory.");
2324
+ return;
2325
+ }
2326
+ const organizationName = await ctx.prompt("Organization name:");
1244
2327
  const platformName = await ctx.prompt("Platform name:");
1245
2328
  const platformDisplayName = await ctx.prompt("Platform display name:");
1246
- await initService({ organizationName, platformName, platformDisplayName }, ctx);
2329
+ const defaultBootstrapSuffix = "bootstrap-service";
2330
+ const defaultCustomizationUiSuffix = "customization-ui";
2331
+ ctx.log(`Default artifact names:`);
2332
+ ctx.log(` Bootstrap service: ${platformName}-${defaultBootstrapSuffix}`);
2333
+ ctx.log(` Customization UI: ${platformName}-${defaultCustomizationUiSuffix}`);
2334
+ const customize = await ctx.confirm("Customize artifact names?", false);
2335
+ let bootstrapServiceSuffix = defaultBootstrapSuffix;
2336
+ let customizationUiSuffix = defaultCustomizationUiSuffix;
2337
+ if (customize) {
2338
+ const bsSuffix = await ctx.prompt(`Bootstrap service suffix (${platformName}-...):`, defaultBootstrapSuffix);
2339
+ if (!/^[a-z0-9-]+$/.test(bsSuffix)) {
2340
+ ctx.log(`Error: Suffix "${bsSuffix}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
2341
+ return;
2342
+ }
2343
+ bootstrapServiceSuffix = bsSuffix;
2344
+ const cuiSuffix = await ctx.prompt(`Customization UI suffix (${platformName}-...):`, defaultCustomizationUiSuffix);
2345
+ if (!/^[a-z0-9-]+$/.test(cuiSuffix)) {
2346
+ ctx.log(`Error: Suffix "${cuiSuffix}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
2347
+ return;
2348
+ }
2349
+ customizationUiSuffix = cuiSuffix;
2350
+ }
2351
+ await initService(
2352
+ { organizationName, platformName, platformDisplayName, bootstrapServiceSuffix, customizationUiSuffix },
2353
+ ctx
2354
+ );
1247
2355
  }
1248
2356
 
1249
2357
  // src/services/configure-idp.service.ts
@@ -1258,15 +2366,12 @@ async function configureIdpUiController(ctx) {
1258
2366
  return;
1259
2367
  }
1260
2368
  const providers = getAllProviders();
1261
- const options = providers.map((p, i) => `${i + 1}: ${p.displayName}`).join(", ");
1262
- const selectionInput = await ctx.prompt(`Select IDP type (${options}):`);
1263
- const index = parseInt(selectionInput.trim(), 10) - 1;
1264
- if (isNaN(index) || index < 0 || index >= providers.length) {
1265
- ctx.log(`Error: Invalid selection "${selectionInput}"`);
1266
- return;
1267
- }
1268
- const provider = providers[index];
1269
- const name = provider.type;
2369
+ const providerType = await ctx.select(
2370
+ "Select IDP type:",
2371
+ providers.map((p) => ({ label: p.displayName, value: p.type }))
2372
+ );
2373
+ const provider = providers.find((p) => p.type === providerType);
2374
+ const name = await ctx.prompt("Provider name:");
1270
2375
  const issuer = await ctx.prompt("Issuer URL:");
1271
2376
  const clientId = await ctx.prompt("Client ID:");
1272
2377
  const clientSecret = await ctx.prompt("Client Secret:");
@@ -1291,19 +2396,38 @@ async function createServiceModuleUiController(ctx) {
1291
2396
  ctx.log("Error: Cannot create a service module \u2014 no platform initialized in this directory.");
1292
2397
  return;
1293
2398
  }
1294
- const applicationName = await ctx.prompt("Application name:");
1295
- if (!/^[a-z0-9-]+$/.test(applicationName)) {
1296
- ctx.log(`Error: Application name "${applicationName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
2399
+ const layout = await findPlatformLayout();
2400
+ if (!layout) {
2401
+ ctx.log("Error: Cannot create a service module \u2014 no platform initialized in this directory.");
1297
2402
  return;
1298
2403
  }
2404
+ let manifest = await readManifest(layout.rootDir, layout.coreDirName);
2405
+ if (manifest.applications.length === 0) {
2406
+ ctx.log("No applications found in this platform.");
2407
+ const createFirst = await ctx.confirm("Would you like to create an application first?");
2408
+ if (!createFirst) return;
2409
+ await createApplicationUiController(ctx);
2410
+ manifest = await readManifest(layout.rootDir, layout.coreDirName);
2411
+ if (manifest.applications.length === 0) {
2412
+ ctx.log("Error: No applications available after creation attempt.");
2413
+ return;
2414
+ }
2415
+ }
2416
+ const applicationName = await ctx.select(
2417
+ "Select application:",
2418
+ manifest.applications.map((a) => ({ label: `${a.name} \u2014 ${a.displayName}`, value: a.name }))
2419
+ );
1299
2420
  const serviceName = await ctx.prompt("Service name:");
1300
2421
  if (!/^[a-z0-9-]+$/.test(serviceName)) {
1301
2422
  ctx.log(`Error: Service name "${serviceName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
1302
2423
  return;
1303
2424
  }
2425
+ const platformName = manifest.product.name;
2426
+ const includeSuffix = await ctx.confirm(`Include "-service" suffix? (${platformName}-${serviceName}-service)`, true);
2427
+ const serviceNameSuffix = includeSuffix ? "service" : null;
1304
2428
  const serviceDisplayName = await ctx.prompt("Service display name:");
1305
2429
  await createServiceModuleService(
1306
- { applicationName, serviceName, serviceDisplayName },
2430
+ { applicationName, serviceName, serviceDisplayName, serviceNameSuffix },
1307
2431
  ctx
1308
2432
  );
1309
2433
  }
@@ -1319,40 +2443,355 @@ async function createUiModuleUiController(ctx) {
1319
2443
  ctx.log("Error: Cannot create a UI module \u2014 no platform initialized in this directory.");
1320
2444
  return;
1321
2445
  }
1322
- const applicationName = await ctx.prompt("Application name:");
1323
- if (!/^[a-z0-9-]+$/.test(applicationName)) {
1324
- ctx.log(`Error: Application name "${applicationName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
2446
+ const layout = await findPlatformLayout();
2447
+ if (!layout) {
2448
+ ctx.log("Error: Cannot create a UI module \u2014 no platform initialized in this directory.");
1325
2449
  return;
1326
2450
  }
2451
+ let manifest = await readManifest(layout.rootDir, layout.coreDirName);
2452
+ if (manifest.applications.length === 0) {
2453
+ ctx.log("No applications found in this platform.");
2454
+ const createFirst = await ctx.confirm("Would you like to create an application first?");
2455
+ if (!createFirst) return;
2456
+ await createApplicationUiController(ctx);
2457
+ manifest = await readManifest(layout.rootDir, layout.coreDirName);
2458
+ if (manifest.applications.length === 0) {
2459
+ ctx.log("Error: No applications available after creation attempt.");
2460
+ return;
2461
+ }
2462
+ }
2463
+ const applicationName = await ctx.select(
2464
+ "Select application:",
2465
+ manifest.applications.map((a) => ({ label: `${a.name} \u2014 ${a.displayName}`, value: a.name }))
2466
+ );
2467
+ const platformName = manifest.product.name;
2468
+ const includeSuffix = await ctx.confirm(`Include "-ui" suffix? (${platformName}-${applicationName}-ui)`, true);
2469
+ const uiModuleSuffix = includeSuffix ? "ui" : null;
1327
2470
  const applicationDisplayName = await ctx.prompt("Application display name:");
1328
2471
  await createUiModuleService(
1329
- { applicationName, applicationDisplayName },
2472
+ { applicationName, applicationDisplayName, uiModuleSuffix },
1330
2473
  ctx
1331
2474
  );
1332
2475
  }
1333
2476
 
2477
+ // src/services/status.service.ts
2478
+ async function statusService() {
2479
+ return gatherStatus();
2480
+ }
2481
+
2482
+ // src/utils/theme.ts
2483
+ import chalk from "chalk";
2484
+ var theme = {
2485
+ prompt: chalk.green,
2486
+ commandName: chalk.cyan,
2487
+ commandDescription: chalk.gray,
2488
+ selected: chalk.bgBlue.white,
2489
+ output: chalk.white,
2490
+ muted: chalk.dim,
2491
+ error: chalk.red,
2492
+ success: chalk.green,
2493
+ warning: chalk.yellow,
2494
+ info: chalk.cyan,
2495
+ section: chalk.bold.white,
2496
+ label: chalk.dim
2497
+ };
2498
+
2499
+ // src/commands/status/status-formatter.ts
2500
+ var CHECK = theme.success("\u2713");
2501
+ var CROSS = theme.error("\u2717");
2502
+ function tick(ok) {
2503
+ return ok ? CHECK : CROSS;
2504
+ }
2505
+ function formatStatusLines(result) {
2506
+ const lines = [];
2507
+ if (result.projectInfo) {
2508
+ const p = result.projectInfo;
2509
+ lines.push(theme.info("\u25B8 ") + theme.section(p.displayName) + theme.label(` (${p.productName})`));
2510
+ lines.push(theme.label(" Organization: ") + p.organization + theme.label(` (${p.scope})`));
2511
+ const appList = p.applicationNames.length > 0 ? p.applicationNames.join(", ") : "(none)";
2512
+ lines.push(theme.label(" Applications: ") + `${p.applicationCount} \u2014 ` + appList);
2513
+ lines.push("");
2514
+ }
2515
+ lines.push(theme.section("Prerequisites"));
2516
+ for (const pre of result.prerequisites) {
2517
+ const ok = pre.available && pre.versionOk;
2518
+ const versionStr = pre.version ? theme.label(` ${pre.version}`) : "";
2519
+ const detailStr = pre.detail ? theme.warning(` \u2014 ${pre.detail}`) : "";
2520
+ lines.push(` ${tick(ok)} ${pre.name}${versionStr}${detailStr}`);
2521
+ }
2522
+ lines.push("");
2523
+ lines.push(theme.section("Lifecycle"));
2524
+ lines.push(` ${tick(result.lifecycle.initialized)} Platform initialized`);
2525
+ const installed = result.lifecycle.installed;
2526
+ lines.push(` ${tick(installed)} Dependencies installed`);
2527
+ if (!installed && result.lifecycle.installedDetails.length > 0) {
2528
+ for (const d of result.lifecycle.installedDetails) {
2529
+ if (!d.exists) {
2530
+ lines.push(` ${CROSS} ${theme.label(d.name)} ${theme.warning("(missing node_modules/)")}`);
2531
+ }
2532
+ }
2533
+ }
2534
+ const built = result.lifecycle.built;
2535
+ lines.push(` ${tick(built)} Build completed`);
2536
+ if (!built && result.lifecycle.builtDetails.length > 0) {
2537
+ for (const d of result.lifecycle.builtDetails) {
2538
+ if (!d.exists) {
2539
+ lines.push(` ${CROSS} ${theme.label(d.name)} ${theme.warning("(missing .turbo/)")}`);
2540
+ }
2541
+ }
2542
+ }
2543
+ lines.push(` ${tick(result.lifecycle.running)} Environment running`);
2544
+ if (result.containers.length > 0) {
2545
+ lines.push("");
2546
+ lines.push(theme.section("Containers"));
2547
+ for (const c of result.containers) {
2548
+ const ok = c.state === "running";
2549
+ const stateStr = ok ? theme.success(c.state) : c.state === "not started" ? theme.label(c.state) : theme.error(c.state);
2550
+ const ports = c.ports ? theme.label(` ${c.ports}`) : "";
2551
+ lines.push(` ${tick(ok)} ${c.name.padEnd(35)} ${stateStr}${ports}`);
2552
+ }
2553
+ }
2554
+ return lines;
2555
+ }
2556
+
2557
+ // src/services/local-script.service.ts
2558
+ async function localScriptService(scriptName, logger, signal, appNames) {
2559
+ await runLocalScript(scriptName, logger, signal, appNames);
2560
+ }
2561
+
2562
+ // src/controllers/ui/status.ui-controller.ts
2563
+ async function statusUiController(ctx) {
2564
+ while (true) {
2565
+ const result = await statusService();
2566
+ for (const line of formatStatusLines(result)) {
2567
+ ctx.log(line);
2568
+ }
2569
+ const { step, appNames, allApps } = computeNextStepInfo(result);
2570
+ if (step === null) {
2571
+ ctx.log("");
2572
+ ctx.log("All checks passed!");
2573
+ return;
2574
+ }
2575
+ if (step === "init") {
2576
+ const shouldRun = await ctx.confirm(`Next step: "init". Run it now?`, true);
2577
+ if (!shouldRun) return;
2578
+ await initUiController(ctx);
2579
+ } else {
2580
+ const appListStr = allApps ? "all apps" : appNames.join(", ");
2581
+ const shouldRun = await ctx.confirm(`Run ${step} for ${appListStr}?`, true);
2582
+ if (!shouldRun) return;
2583
+ await localScriptService(step, ctx, ctx.signal, allApps ? void 0 : appNames);
2584
+ }
2585
+ const resultAfter = await statusService();
2586
+ const nextAfter = computeNextStepInfo(resultAfter);
2587
+ const sameStep = nextAfter.step === step;
2588
+ const noProgress = sameStep && (allApps ? nextAfter.allApps : appNames.some((a) => nextAfter.appNames.includes(a)));
2589
+ if (noProgress) {
2590
+ ctx.log("");
2591
+ ctx.log(`"${step}" did not complete successfully. Check the output above for errors.`);
2592
+ return;
2593
+ }
2594
+ }
2595
+ }
2596
+
2597
+ // src/controllers/ui/local-script.ui-controller.ts
2598
+ function createLocalScriptUiController(scriptName) {
2599
+ return async (ctx) => {
2600
+ const layout = await findPlatformLayout();
2601
+ if (!layout) {
2602
+ ctx.log(`Error: Cannot run "${scriptName}" \u2014 no platform initialized in this directory.`);
2603
+ return;
2604
+ }
2605
+ let manifest;
2606
+ try {
2607
+ manifest = await readManifest(layout.rootDir, layout.coreDirName);
2608
+ } catch (err) {
2609
+ ctx.log(`Error: Could not read product manifest \u2014 ${formatError(err)}`);
2610
+ return;
2611
+ }
2612
+ if (manifest.applications.length === 0) {
2613
+ await localScriptService(scriptName, ctx, ctx.signal);
2614
+ return;
2615
+ }
2616
+ const options = [
2617
+ { label: "Platform (Core)", value: CORE_APP_NAME },
2618
+ ...manifest.applications.map((app) => ({
2619
+ label: `${app.displayName} (${app.name})`,
2620
+ value: app.name
2621
+ }))
2622
+ ];
2623
+ const selected = await ctx.multiselect(
2624
+ `Select targets to ${scriptName}:`,
2625
+ options
2626
+ );
2627
+ if (selected.length === 0) {
2628
+ ctx.log("Nothing selected. Nothing to do.");
2629
+ return;
2630
+ }
2631
+ await localScriptService(scriptName, ctx, ctx.signal, selected);
2632
+ };
2633
+ }
2634
+
2635
+ // src/controllers/ui/manage-platform-admins.ui-controller.ts
2636
+ import { execSync } from "child_process";
2637
+
2638
+ // src/services/manage-platform-admins.service.ts
2639
+ async function listAdminsService(logger) {
2640
+ return listPlatformAdmins(logger);
2641
+ }
2642
+ async function addAdminService(username, logger) {
2643
+ return addPlatformAdmin(username, logger);
2644
+ }
2645
+ async function removeAdminService(ruleId, logger) {
2646
+ return removePlatformAdmin(ruleId, logger);
2647
+ }
2648
+
2649
+ // src/controllers/ui/manage-platform-admins.ui-controller.ts
2650
+ function getDefaultUsername() {
2651
+ try {
2652
+ const email = execSync("git config user.email", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
2653
+ return email || void 0;
2654
+ } catch {
2655
+ return void 0;
2656
+ }
2657
+ }
2658
+ async function managePlatformAdminsUiController(ctx) {
2659
+ if (!await isPlatformInitialized()) {
2660
+ ctx.log("Error: Cannot manage platform admins \u2014 no platform initialized in this directory.");
2661
+ return;
2662
+ }
2663
+ while (true) {
2664
+ const action = await ctx.select("What would you like to do?", [
2665
+ { label: "List current admins", value: "list" },
2666
+ { label: "Add admin(s)", value: "add" },
2667
+ { label: "Remove admin(s)", value: "remove" },
2668
+ { label: "Back to main menu", value: "back" }
2669
+ ]);
2670
+ if (action === "list") {
2671
+ await handleList(ctx);
2672
+ } else if (action === "add") {
2673
+ await handleAdd(ctx);
2674
+ } else if (action === "remove") {
2675
+ await handleRemove(ctx);
2676
+ } else if (action === "back") {
2677
+ break;
2678
+ }
2679
+ }
2680
+ }
2681
+ async function handleList(ctx) {
2682
+ const admins = await listAdminsService(ctx);
2683
+ if (admins.length === 0) {
2684
+ ctx.log("No platform admins found.");
2685
+ } else {
2686
+ ctx.log("Current platform admins:");
2687
+ for (const admin of admins) {
2688
+ ctx.log(` - ${admin.username}`);
2689
+ }
2690
+ }
2691
+ }
2692
+ async function handleAdd(ctx) {
2693
+ const currentAdmins = await listAdminsService(ctx);
2694
+ const existingUsernames = new Set(currentAdmins.map((a) => a.username));
2695
+ const pendingAdmins = [];
2696
+ const suggestedUsername = getDefaultUsername();
2697
+ const effectiveSuggestion = suggestedUsername && !existingUsernames.has(suggestedUsername) ? suggestedUsername : void 0;
2698
+ let isFirstIteration = true;
2699
+ while (true) {
2700
+ const defaultValue = isFirstIteration ? effectiveSuggestion : void 0;
2701
+ const username = (await ctx.prompt("Username to add as admin:", defaultValue)).trim();
2702
+ isFirstIteration = false;
2703
+ if (!username) {
2704
+ ctx.log("Username cannot be empty, skipping.");
2705
+ } else if (existingUsernames.has(username)) {
2706
+ ctx.log(`'${username}' is already a platform admin.`);
2707
+ } else if (pendingAdmins.includes(username)) {
2708
+ ctx.log(`'${username}' is already in the pending list.`);
2709
+ } else {
2710
+ pendingAdmins.push(username);
2711
+ ctx.log(`Admins to add: ${pendingAdmins.join(", ")}`);
2712
+ }
2713
+ const addAnother = await ctx.confirm("Add another admin?", false);
2714
+ if (!addAnother) break;
2715
+ }
2716
+ if (pendingAdmins.length === 0) {
2717
+ ctx.log("No admins to add.");
2718
+ return;
2719
+ }
2720
+ let successCount = 0;
2721
+ for (const username of pendingAdmins) {
2722
+ const ok = await addAdminService(username, ctx);
2723
+ if (ok) {
2724
+ ctx.log(`'${username}' granted platform admin access.`);
2725
+ successCount++;
2726
+ }
2727
+ }
2728
+ ctx.log(`Successfully added ${successCount} of ${pendingAdmins.length} admin(s).`);
2729
+ }
2730
+ async function handleRemove(ctx) {
2731
+ const admins = await listAdminsService(ctx);
2732
+ if (admins.length === 0) {
2733
+ ctx.log("No platform admins to remove.");
2734
+ return;
2735
+ }
2736
+ const selected = await ctx.multiselect(
2737
+ "Select admins to remove:",
2738
+ admins.map((a) => ({ label: a.username, value: a.ruleId }))
2739
+ );
2740
+ if (selected.length === 0) {
2741
+ ctx.log("No admins selected.");
2742
+ return;
2743
+ }
2744
+ const confirmed = await ctx.confirm(`Remove ${selected.length} admin(s)?`, false);
2745
+ if (!confirmed) {
2746
+ ctx.log("Cancelled.");
2747
+ return;
2748
+ }
2749
+ let successCount = 0;
2750
+ for (const ruleId of selected) {
2751
+ const admin = admins.find((a) => a.ruleId === ruleId);
2752
+ const ok = await removeAdminService(ruleId, ctx);
2753
+ if (ok) {
2754
+ ctx.log(`'${admin?.username ?? ruleId}' removed from platform admins.`);
2755
+ successCount++;
2756
+ }
2757
+ }
2758
+ ctx.log(`Successfully removed ${successCount} of ${selected.length} admin(s).`);
2759
+ }
2760
+
1334
2761
  // src/controllers/ui/registry.ts
1335
2762
  var uiControllers = /* @__PURE__ */ new Map([
1336
2763
  [CREATE_APPLICATION_COMMAND_NAME, createApplicationUiController],
1337
2764
  [INIT_COMMAND_NAME, initUiController],
1338
2765
  [CONFIGURE_IDP_COMMAND_NAME, configureIdpUiController],
1339
2766
  [CREATE_SERVICE_MODULE_COMMAND_NAME, createServiceModuleUiController],
1340
- [CREATE_UI_MODULE_COMMAND_NAME, createUiModuleUiController]
2767
+ [CREATE_UI_MODULE_COMMAND_NAME, createUiModuleUiController],
2768
+ [STATUS_COMMAND_NAME, statusUiController],
2769
+ [INSTALL_COMMAND_NAME, createLocalScriptUiController(INSTALL_COMMAND_NAME)],
2770
+ [BUILD_COMMAND_NAME, createLocalScriptUiController(BUILD_COMMAND_NAME)],
2771
+ [START_COMMAND_NAME, createLocalScriptUiController(START_COMMAND_NAME)],
2772
+ [STOP_COMMAND_NAME, createLocalScriptUiController(STOP_COMMAND_NAME)],
2773
+ [DESTROY_COMMAND_NAME, createLocalScriptUiController(DESTROY_COMMAND_NAME)],
2774
+ [MANAGE_PLATFORM_ADMINS_COMMAND_NAME, managePlatformAdminsUiController]
1341
2775
  ]);
1342
2776
 
1343
2777
  // src/hooks/use-command-runner.ts
1344
- function useCommandRunner({ appendStaticItem, setState }) {
2778
+ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
1345
2779
  const outputCountRef = useRef(0);
1346
2780
  const abortControllerRef = useRef(null);
1347
2781
  const promptResolveRef = useRef(null);
1348
2782
  const [promptMessage, setPromptMessage] = useState2("");
1349
2783
  const [promptValue, setPromptValue] = useState2("");
2784
+ const [promptMode, setPromptMode] = useState2({ kind: "text" });
2785
+ const [selectIndex, setSelectIndex] = useState2(0);
2786
+ const [confirmValue, setConfirmValue] = useState2(true);
2787
+ const [multiselectChecked, setMultiselectChecked] = useState2(/* @__PURE__ */ new Set());
1350
2788
  const abortExecution = useCallback(() => {
1351
2789
  abortControllerRef.current?.abort();
1352
2790
  abortControllerRef.current = null;
1353
2791
  promptResolveRef.current = null;
2792
+ setPromptMode({ kind: "text" });
1354
2793
  }, []);
1355
- const runCommand = useCallback(
2794
+ const runCommand2 = useCallback(
1356
2795
  (cmd) => {
1357
2796
  const controller = new AbortController();
1358
2797
  abortControllerRef.current = controller;
@@ -1365,15 +2804,73 @@ function useCommandRunner({ appendStaticItem, setState }) {
1365
2804
  appendStaticItem({ kind: "output", id, line: message });
1366
2805
  }
1367
2806
  },
1368
- prompt(message) {
1369
- return new Promise((resolve, reject) => {
2807
+ prompt(message, defaultValue) {
2808
+ return new Promise((resolve6, reject) => {
2809
+ if (controller.signal.aborted) {
2810
+ reject(new DOMException("Aborted", "AbortError"));
2811
+ return;
2812
+ }
2813
+ setPromptMessage(message);
2814
+ setPromptValue(defaultValue ?? "");
2815
+ setPromptMode({ kind: "text" });
2816
+ promptResolveRef.current = resolve6;
2817
+ setState(APP_STATE.PROMPTING);
2818
+ controller.signal.addEventListener(
2819
+ "abort",
2820
+ () => reject(new DOMException("Aborted", "AbortError")),
2821
+ { once: true }
2822
+ );
2823
+ });
2824
+ },
2825
+ select(message, options) {
2826
+ return new Promise((resolve6, reject) => {
2827
+ if (controller.signal.aborted) {
2828
+ reject(new DOMException("Aborted", "AbortError"));
2829
+ return;
2830
+ }
2831
+ setPromptMessage(message);
2832
+ setPromptMode({ kind: "select", options });
2833
+ setSelectIndex(0);
2834
+ promptResolveRef.current = resolve6;
2835
+ setState(APP_STATE.PROMPTING);
2836
+ controller.signal.addEventListener(
2837
+ "abort",
2838
+ () => reject(new DOMException("Aborted", "AbortError")),
2839
+ { once: true }
2840
+ );
2841
+ });
2842
+ },
2843
+ multiselect(message, options) {
2844
+ return new Promise((resolve6, reject) => {
2845
+ if (controller.signal.aborted) {
2846
+ reject(new DOMException("Aborted", "AbortError"));
2847
+ return;
2848
+ }
2849
+ setPromptMessage(message);
2850
+ setPromptMode({ kind: "multiselect", options });
2851
+ setSelectIndex(0);
2852
+ setMultiselectChecked(new Set(options.map((_, i) => i)));
2853
+ promptResolveRef.current = (value) => {
2854
+ resolve6(value ? value.split(",") : []);
2855
+ };
2856
+ setState(APP_STATE.PROMPTING);
2857
+ controller.signal.addEventListener(
2858
+ "abort",
2859
+ () => reject(new DOMException("Aborted", "AbortError")),
2860
+ { once: true }
2861
+ );
2862
+ });
2863
+ },
2864
+ confirm(message, defaultValue) {
2865
+ return new Promise((resolve6, reject) => {
1370
2866
  if (controller.signal.aborted) {
1371
2867
  reject(new DOMException("Aborted", "AbortError"));
1372
2868
  return;
1373
2869
  }
1374
2870
  setPromptMessage(message);
1375
- setPromptValue("");
1376
- promptResolveRef.current = resolve;
2871
+ setPromptMode({ kind: "confirm" });
2872
+ setConfirmValue(defaultValue ?? true);
2873
+ promptResolveRef.current = (value) => resolve6(value === "true");
1377
2874
  setState(APP_STATE.PROMPTING);
1378
2875
  controller.signal.addEventListener(
1379
2876
  "abort",
@@ -1393,37 +2890,47 @@ function useCommandRunner({ appendStaticItem, setState }) {
1393
2890
  uiController(ctx).then(() => {
1394
2891
  if (!controller.signal.aborted) {
1395
2892
  setState(APP_STATE.IDLE);
2893
+ onCommandComplete?.();
1396
2894
  }
1397
2895
  }).catch((err) => {
1398
2896
  if (controller.signal.aborted) return;
1399
2897
  const id = `output-${outputCountRef.current++}`;
1400
2898
  appendStaticItem({ kind: "output", id, line: `Error: ${formatError(err)}` });
1401
2899
  setState(APP_STATE.IDLE);
2900
+ onCommandComplete?.();
1402
2901
  });
1403
2902
  },
1404
- [appendStaticItem, setState]
2903
+ [appendStaticItem, setState, onCommandComplete]
1405
2904
  );
1406
2905
  const handlePromptSubmit = useCallback(
1407
2906
  (value) => {
1408
- const resolve = promptResolveRef.current;
2907
+ const resolve6 = promptResolveRef.current;
1409
2908
  promptResolveRef.current = null;
2909
+ setPromptMode({ kind: "text" });
1410
2910
  setState(APP_STATE.EXECUTING);
1411
- resolve?.(value);
2911
+ resolve6?.(value);
1412
2912
  },
1413
2913
  [setState]
1414
2914
  );
1415
2915
  return {
1416
- runCommand,
2916
+ runCommand: runCommand2,
1417
2917
  handlePromptSubmit,
1418
2918
  abortExecution,
1419
2919
  promptMessage,
1420
2920
  promptValue,
1421
- setPromptValue
2921
+ setPromptValue,
2922
+ promptMode,
2923
+ selectIndex,
2924
+ setSelectIndex,
2925
+ confirmValue,
2926
+ setConfirmValue,
2927
+ multiselectChecked,
2928
+ setMultiselectChecked
1422
2929
  };
1423
2930
  }
1424
2931
 
1425
2932
  // src/app.tsx
1426
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2933
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
1427
2934
  var require2 = createRequire(import.meta.url);
1428
2935
  var { version } = require2("../package.json");
1429
2936
  function App() {
@@ -1432,12 +2939,35 @@ function App() {
1432
2939
  const [inputValue, setInputValue] = useState3("");
1433
2940
  const [selectedIndex, setSelectedIndex] = useState3(0);
1434
2941
  const [staticItems, setStaticItems] = useState3([{ kind: "banner", id: "banner" }]);
2942
+ const [platformInitialized, setPlatformInitialized] = useState3(false);
2943
+ useEffect2(() => {
2944
+ isPlatformInitialized().then(setPlatformInitialized).catch(() => {
2945
+ });
2946
+ }, []);
1435
2947
  const appendStaticItem = useCallback2((item) => {
1436
2948
  setStaticItems((prev) => [...prev, item]);
1437
2949
  }, []);
1438
- const { runCommand, handlePromptSubmit, abortExecution, promptMessage, promptValue, setPromptValue } = useCommandRunner({ appendStaticItem, setState });
2950
+ const handleCommandComplete = useCallback2(() => {
2951
+ isPlatformInitialized().then(setPlatformInitialized).catch(() => {
2952
+ });
2953
+ }, []);
2954
+ const {
2955
+ runCommand: runCommand2,
2956
+ handlePromptSubmit,
2957
+ abortExecution,
2958
+ promptMessage,
2959
+ promptValue,
2960
+ setPromptValue,
2961
+ promptMode,
2962
+ selectIndex,
2963
+ setSelectIndex,
2964
+ confirmValue,
2965
+ setConfirmValue,
2966
+ multiselectChecked,
2967
+ setMultiselectChecked
2968
+ } = useCommandRunner({ appendStaticItem, setState, onCommandComplete: handleCommandComplete });
1439
2969
  const query = inputValue.startsWith("/") ? inputValue.slice(1) : "";
1440
- const filteredCommands = registry.search(query);
2970
+ const filteredCommands = registry.searchVisible(query, { platformInitialized });
1441
2971
  useInput(
1442
2972
  (input, key) => {
1443
2973
  if (key.ctrl && input === "c") {
@@ -1449,6 +2979,73 @@ function App() {
1449
2979
  setState(APP_STATE.IDLE);
1450
2980
  return;
1451
2981
  }
2982
+ if (state === APP_STATE.PROMPTING) {
2983
+ if (promptMode.kind === "select") {
2984
+ if (key.upArrow) {
2985
+ setSelectIndex((prev) => Math.max(0, prev - 1));
2986
+ return;
2987
+ }
2988
+ if (key.downArrow) {
2989
+ setSelectIndex((prev) => Math.min(promptMode.options.length - 1, prev + 1));
2990
+ return;
2991
+ }
2992
+ if (key.return) {
2993
+ const selected = promptMode.options[selectIndex];
2994
+ if (selected) handlePromptSubmit(selected.value);
2995
+ return;
2996
+ }
2997
+ return;
2998
+ }
2999
+ if (promptMode.kind === "multiselect") {
3000
+ if (key.upArrow) {
3001
+ setSelectIndex((prev) => Math.max(0, prev - 1));
3002
+ return;
3003
+ }
3004
+ if (key.downArrow) {
3005
+ setSelectIndex((prev) => Math.min(promptMode.options.length, prev + 1));
3006
+ return;
3007
+ }
3008
+ if (input === " ") {
3009
+ if (selectIndex === 0) {
3010
+ const allChecked = multiselectChecked.size === promptMode.options.length;
3011
+ setMultiselectChecked(
3012
+ allChecked ? /* @__PURE__ */ new Set() : new Set(promptMode.options.map((_, i) => i))
3013
+ );
3014
+ } else {
3015
+ const optIdx = selectIndex - 1;
3016
+ setMultiselectChecked((prev) => {
3017
+ const next = new Set(prev);
3018
+ if (next.has(optIdx)) next.delete(optIdx);
3019
+ else next.add(optIdx);
3020
+ return next;
3021
+ });
3022
+ }
3023
+ return;
3024
+ }
3025
+ if (key.return) {
3026
+ const selected = promptMode.options.filter((_, i) => multiselectChecked.has(i)).map((o) => o.value).join(",");
3027
+ handlePromptSubmit(selected);
3028
+ return;
3029
+ }
3030
+ return;
3031
+ }
3032
+ if (promptMode.kind === "confirm") {
3033
+ if (key.leftArrow || key.upArrow) {
3034
+ setConfirmValue(true);
3035
+ return;
3036
+ }
3037
+ if (key.rightArrow || key.downArrow) {
3038
+ setConfirmValue(false);
3039
+ return;
3040
+ }
3041
+ if (key.return) {
3042
+ handlePromptSubmit(confirmValue ? "true" : "false");
3043
+ return;
3044
+ }
3045
+ return;
3046
+ }
3047
+ return;
3048
+ }
1452
3049
  if (state === APP_STATE.PALETTE) {
1453
3050
  if (key.escape) {
1454
3051
  setState(APP_STATE.IDLE);
@@ -1467,7 +3064,7 @@ function App() {
1467
3064
  const cmd = filteredCommands[selectedIndex];
1468
3065
  if (cmd) {
1469
3066
  setInputValue("");
1470
- runCommand(cmd);
3067
+ runCommand2(cmd);
1471
3068
  }
1472
3069
  return;
1473
3070
  }
@@ -1503,24 +3100,34 @@ function App() {
1503
3100
  if (!value.startsWith("/")) return;
1504
3101
  const name = value.slice(1).trim();
1505
3102
  const cmd = registry.get(name);
1506
- if (cmd) runCommand(cmd);
3103
+ if (cmd) runCommand2(cmd);
1507
3104
  },
1508
- [runCommand]
3105
+ [runCommand2]
1509
3106
  );
1510
3107
  function renderActiveArea() {
1511
3108
  switch (state) {
1512
3109
  case APP_STATE.EXECUTING:
1513
- return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "row", gap: 1, children: [
1514
- /* @__PURE__ */ jsx7(Spinner, { label: "Running\u2026" }),
1515
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "(esc to cancel)" })
3110
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "row", gap: 1, children: [
3111
+ /* @__PURE__ */ jsx10(Spinner, { label: "Running\u2026" }),
3112
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "(esc to cancel)" })
1516
3113
  ] });
1517
3114
  case APP_STATE.PROMPTING:
1518
- return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", children: [
1519
- /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: promptMessage }),
1520
- /* @__PURE__ */ jsx7(Prompt, { value: promptValue, onChange: setPromptValue, onSubmit: handlePromptSubmit })
3115
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
3116
+ /* @__PURE__ */ jsx10(Text10, { color: "cyan", children: promptMessage }),
3117
+ promptMode.kind === "select" && /* @__PURE__ */ jsx10(SelectList, { options: promptMode.options, selectedIndex: selectIndex }),
3118
+ promptMode.kind === "multiselect" && /* @__PURE__ */ jsx10(
3119
+ MultiSelectList,
3120
+ {
3121
+ options: promptMode.options,
3122
+ focusedIndex: selectIndex,
3123
+ checkedIndices: multiselectChecked
3124
+ }
3125
+ ),
3126
+ promptMode.kind === "confirm" && /* @__PURE__ */ jsx10(ConfirmPrompt, { value: confirmValue }),
3127
+ promptMode.kind === "text" && /* @__PURE__ */ jsx10(Prompt, { value: promptValue, onChange: setPromptValue, onSubmit: handlePromptSubmit })
1521
3128
  ] });
1522
3129
  default:
1523
- return /* @__PURE__ */ jsx7(
3130
+ return /* @__PURE__ */ jsx10(
1524
3131
  Prompt,
1525
3132
  {
1526
3133
  value: inputValue,
@@ -1533,10 +3140,10 @@ function App() {
1533
3140
  );
1534
3141
  }
1535
3142
  }
1536
- return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", children: [
1537
- /* @__PURE__ */ jsx7(ScrollbackHistory, { items: staticItems, version }),
3143
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
3144
+ /* @__PURE__ */ jsx10(ScrollbackHistory, { items: staticItems, version }),
1538
3145
  renderActiveArea(),
1539
- state === APP_STATE.PALETTE && /* @__PURE__ */ jsx7(CommandPalette, { commands: filteredCommands, selectedIndex })
3146
+ state === APP_STATE.PALETTE && /* @__PURE__ */ jsx10(CommandPalette, { commands: filteredCommands, selectedIndex })
1540
3147
  ] });
1541
3148
  }
1542
3149
 
@@ -1561,21 +3168,8 @@ async function createApplicationCliController(args2) {
1561
3168
  console.error(`Error: Application name "${applicationName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
1562
3169
  process.exit(1);
1563
3170
  }
1564
- let organizationName;
1565
- let platformName;
1566
- try {
1567
- const manifest = await readPlatformManifest();
1568
- organizationName = manifest.organizationName;
1569
- platformName = manifest.platformName;
1570
- } catch (err) {
1571
- const message = err instanceof Error ? err.message : String(err);
1572
- console.error(`Error: Could not read .platform.json \u2014 ${message}`);
1573
- process.exit(1);
1574
- }
1575
3171
  await createApplicationService(
1576
3172
  {
1577
- organizationName,
1578
- platformName,
1579
3173
  applicationName,
1580
3174
  applicationDisplayName,
1581
3175
  applicationDescription,
@@ -1592,13 +3186,13 @@ async function initCliController(args2) {
1592
3186
  console.error("Error: Cannot initialize a new platform \u2014 a platform is already initialized in this directory.");
1593
3187
  process.exit(1);
1594
3188
  }
1595
- const { organizationName, platformName, platformDisplayName } = args2;
3189
+ const { organizationName, platformName, platformDisplayName, bootstrapServiceSuffix, customizationUiSuffix } = args2;
1596
3190
  if (!organizationName || !platformName || !platformDisplayName) {
1597
3191
  console.error("Error: organizationName, platformName, and platformDisplayName are required.");
1598
3192
  process.exit(1);
1599
3193
  }
1600
3194
  await initService(
1601
- { organizationName, platformName, platformDisplayName },
3195
+ { organizationName, platformName, platformDisplayName, bootstrapServiceSuffix, customizationUiSuffix },
1602
3196
  { log: console.log }
1603
3197
  );
1604
3198
  }
@@ -1610,9 +3204,9 @@ async function configureIdpCliController(args2) {
1610
3204
  console.error("Error: Cannot configure an IDP \u2014 no platform initialized in this directory.");
1611
3205
  process.exit(1);
1612
3206
  }
1613
- const { providerType, issuer, clientId, clientSecret } = args2;
1614
- if (!providerType || !issuer || !clientId || !clientSecret) {
1615
- logger.log("Error: Missing required arguments: providerType, issuer, clientId, clientSecret");
3207
+ const { providerType, name, issuer, clientId, clientSecret } = args2;
3208
+ if (!providerType || !name || !issuer || !clientId || !clientSecret) {
3209
+ logger.log("Error: Missing required arguments: providerType, name, issuer, clientId, clientSecret");
1616
3210
  process.exit(1);
1617
3211
  }
1618
3212
  const provider = idpProviderRegistry.get(providerType);
@@ -1629,83 +3223,9 @@ async function configureIdpCliController(args2) {
1629
3223
  }
1630
3224
  extras[field.key] = value;
1631
3225
  }
1632
- const name = providerType;
1633
3226
  await configureIdpService({ providerType, name, issuer, clientId, clientSecret, extras }, logger);
1634
3227
  }
1635
3228
 
1636
- // src/utils/run-npm-script.ts
1637
- import { spawn } from "child_process";
1638
- import { access as access4 } from "fs/promises";
1639
- import { join as join21 } from "path";
1640
- async function runNpmScript(scriptName, logger, signal) {
1641
- let localDir;
1642
- try {
1643
- const { platformName } = await readPlatformManifest();
1644
- localDir = join21(process.cwd(), `${platformName}-local`);
1645
- await access4(localDir);
1646
- } catch {
1647
- logger.log(`Error: No initialized platform found in this directory. Run "platform init" first.`);
1648
- return;
1649
- }
1650
- return new Promise((resolve) => {
1651
- const child = spawn("npm", ["run", scriptName], {
1652
- cwd: localDir,
1653
- shell: true,
1654
- stdio: ["ignore", "pipe", "pipe"]
1655
- });
1656
- const onAbort = () => {
1657
- child.kill("SIGTERM");
1658
- };
1659
- if (signal) {
1660
- if (signal.aborted) {
1661
- child.kill("SIGTERM");
1662
- resolve();
1663
- return;
1664
- }
1665
- signal.addEventListener("abort", onAbort, { once: true });
1666
- }
1667
- child.stdout.on("data", (data) => {
1668
- for (const line of data.toString().split("\n")) {
1669
- if (line.trim()) logger.log(line);
1670
- }
1671
- });
1672
- child.stderr.on("data", (data) => {
1673
- for (const line of data.toString().split("\n")) {
1674
- if (line.trim()) logger.log(line);
1675
- }
1676
- });
1677
- child.on("close", (code, sig) => {
1678
- signal?.removeEventListener("abort", onAbort);
1679
- if (sig === "SIGTERM" || signal?.aborted) {
1680
- logger.log(`Command cancelled.`);
1681
- } else if (code !== 0) {
1682
- logger.log(`Command "npm run ${scriptName}" exited with code ${code}.`);
1683
- }
1684
- resolve();
1685
- });
1686
- child.on("error", (err) => {
1687
- signal?.removeEventListener("abort", onAbort);
1688
- logger.log(`Failed to run "npm run ${scriptName}": ${err.message}`);
1689
- resolve();
1690
- });
1691
- });
1692
- }
1693
-
1694
- // src/commands/local-scripts/local-script.command.ts
1695
- var INSTALL_COMMAND_NAME = "install";
1696
- var BUILD_COMMAND_NAME = "build";
1697
- var START_COMMAND_NAME = "start";
1698
- var STOP_COMMAND_NAME = "stop";
1699
- var DESTROY_COMMAND_NAME = "destroy";
1700
- async function runLocalScript(scriptName, logger, signal) {
1701
- await runNpmScript(scriptName, logger, signal);
1702
- }
1703
-
1704
- // src/services/local-script.service.ts
1705
- async function localScriptService(scriptName, logger, signal) {
1706
- await runLocalScript(scriptName, logger, signal);
1707
- }
1708
-
1709
3229
  // src/utils/cli-spinner.ts
1710
3230
  var FRAMES2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1711
3231
  var INTERVAL_MS2 = 80;
@@ -1741,10 +3261,11 @@ function createCliSpinner(label) {
1741
3261
 
1742
3262
  // src/controllers/cli/local-script.cli-controller.ts
1743
3263
  function createLocalScriptCliController(scriptName) {
1744
- return async (_args) => {
3264
+ return async (args2) => {
1745
3265
  const spinner = createCliSpinner(`Running ${scriptName}\u2026`);
1746
3266
  spinner.start();
1747
- await localScriptService(scriptName, { log: (msg) => spinner.log(msg) });
3267
+ const appNames = args2._positional ? JSON.parse(args2._positional) : void 0;
3268
+ await localScriptService(scriptName, { log: (msg) => spinner.log(msg) }, void 0, appNames);
1748
3269
  spinner.stop();
1749
3270
  };
1750
3271
  }
@@ -1795,6 +3316,88 @@ async function createUiModuleCliController(args2) {
1795
3316
  );
1796
3317
  }
1797
3318
 
3319
+ // src/controllers/cli/status.cli-controller.ts
3320
+ var statusCliController = async (_args) => {
3321
+ const result = await statusService();
3322
+ const lines = formatStatusLines(result);
3323
+ for (const line of lines) {
3324
+ console.log(line);
3325
+ }
3326
+ const { step, appNames, allApps } = computeNextStepInfo(result);
3327
+ if (step !== null) {
3328
+ console.log("");
3329
+ if (step === "init" || allApps) {
3330
+ console.log(`Hint: Run "platform ${step}" to continue.`);
3331
+ } else {
3332
+ console.log(`Hint: Run "platform ${step} ${appNames.join(" ")}" to continue.`);
3333
+ }
3334
+ process.exit(1);
3335
+ }
3336
+ };
3337
+
3338
+ // src/controllers/cli/manage-platform-admins.cli-controller.ts
3339
+ async function managePlatformAdminsCliController(args2) {
3340
+ const logger = { log: console.log };
3341
+ if (!await isPlatformInitialized()) {
3342
+ console.error("Error: Cannot manage platform admins \u2014 no platform initialized in this directory.");
3343
+ process.exit(1);
3344
+ }
3345
+ const { action } = args2;
3346
+ if (!action || !["list", "add", "remove"].includes(action)) {
3347
+ logger.log("Error: Missing or invalid 'action' argument. Valid values: list, add, remove");
3348
+ logger.log("Usage:");
3349
+ logger.log(" platform manage-platform-admins action=list");
3350
+ logger.log(" platform manage-platform-admins action=add usernames=alice,bob");
3351
+ logger.log(" platform manage-platform-admins action=remove usernames=alice");
3352
+ process.exit(1);
3353
+ }
3354
+ if (action === "list") {
3355
+ const admins = await listAdminsService(logger);
3356
+ if (admins.length === 0) {
3357
+ logger.log("No platform admins found.");
3358
+ } else {
3359
+ logger.log("Current platform admins:");
3360
+ for (const admin of admins) {
3361
+ logger.log(` - ${admin.username}`);
3362
+ }
3363
+ }
3364
+ return;
3365
+ }
3366
+ const { usernames } = args2;
3367
+ if (!usernames) {
3368
+ logger.log(`Error: Missing required argument 'usernames' for action '${action}'`);
3369
+ process.exit(1);
3370
+ }
3371
+ const usernameList = usernames.split(",").map((u) => u.trim()).filter(Boolean);
3372
+ if (usernameList.length === 0) {
3373
+ logger.log("Error: 'usernames' argument is empty.");
3374
+ process.exit(1);
3375
+ }
3376
+ if (action === "add") {
3377
+ for (const username of usernameList) {
3378
+ const ok = await addAdminService(username, logger);
3379
+ if (ok) {
3380
+ logger.log(`'${username}' granted platform admin access.`);
3381
+ }
3382
+ }
3383
+ return;
3384
+ }
3385
+ if (action === "remove") {
3386
+ const admins = await listAdminsService(logger);
3387
+ for (const username of usernameList) {
3388
+ const admin = admins.find((a) => a.username === username);
3389
+ if (!admin) {
3390
+ logger.log(`'${username}' is not currently a platform admin.`);
3391
+ continue;
3392
+ }
3393
+ const ok = await removeAdminService(admin.ruleId, logger);
3394
+ if (ok) {
3395
+ logger.log(`'${username}' removed from platform admins.`);
3396
+ }
3397
+ }
3398
+ }
3399
+ }
3400
+
1798
3401
  // src/controllers/cli/registry.ts
1799
3402
  var cliControllers = /* @__PURE__ */ new Map([
1800
3403
  [CREATE_APPLICATION_COMMAND_NAME, createApplicationCliController],
@@ -1802,32 +3405,40 @@ var cliControllers = /* @__PURE__ */ new Map([
1802
3405
  [CONFIGURE_IDP_COMMAND_NAME, configureIdpCliController],
1803
3406
  [CREATE_SERVICE_MODULE_COMMAND_NAME, createServiceModuleCliController],
1804
3407
  [CREATE_UI_MODULE_COMMAND_NAME, createUiModuleCliController],
3408
+ [STATUS_COMMAND_NAME, statusCliController],
1805
3409
  [INSTALL_COMMAND_NAME, createLocalScriptCliController(INSTALL_COMMAND_NAME)],
1806
3410
  [BUILD_COMMAND_NAME, createLocalScriptCliController(BUILD_COMMAND_NAME)],
1807
3411
  [START_COMMAND_NAME, createLocalScriptCliController(START_COMMAND_NAME)],
1808
3412
  [STOP_COMMAND_NAME, createLocalScriptCliController(STOP_COMMAND_NAME)],
1809
- [DESTROY_COMMAND_NAME, createLocalScriptCliController(DESTROY_COMMAND_NAME)]
3413
+ [DESTROY_COMMAND_NAME, createLocalScriptCliController(DESTROY_COMMAND_NAME)],
3414
+ [MANAGE_PLATFORM_ADMINS_COMMAND_NAME, managePlatformAdminsCliController]
1810
3415
  ]);
1811
3416
 
1812
3417
  // src/utils/parse-args.ts
1813
3418
  function parseKeyValueArgs(args2) {
1814
3419
  const result = {};
3420
+ const positional = [];
1815
3421
  for (const arg of args2) {
1816
3422
  const eqIndex = arg.indexOf("=");
1817
3423
  if (eqIndex > 0) {
1818
3424
  const key = arg.slice(0, eqIndex);
1819
3425
  const value = arg.slice(eqIndex + 1);
1820
3426
  result[key] = value;
3427
+ } else {
3428
+ positional.push(arg);
1821
3429
  }
1822
3430
  }
3431
+ if (positional.length > 0) {
3432
+ result._positional = JSON.stringify(positional);
3433
+ }
1823
3434
  return result;
1824
3435
  }
1825
3436
 
1826
3437
  // src/index.tsx
1827
- import { jsx as jsx8 } from "react/jsx-runtime";
3438
+ import { jsx as jsx11 } from "react/jsx-runtime";
1828
3439
  var args = process.argv.slice(2);
1829
3440
  if (args.length === 0) {
1830
- render(/* @__PURE__ */ jsx8(App, {}));
3441
+ render(/* @__PURE__ */ jsx11(App, {}));
1831
3442
  } else {
1832
3443
  const commandName = args[0];
1833
3444
  const params = parseKeyValueArgs(args.slice(1));