@bluealba/platform-cli 0.3.0 → 0.3.1-develop-233

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 (36) hide show
  1. package/dist/index.js +2094 -471
  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/Dockerfile +4 -4
  8. package/templates/customization-ui-module-template/Dockerfile.development +1 -1
  9. package/templates/customization-ui-module-template/package.json +6 -6
  10. package/templates/customization-ui-module-template/tsconfig.json +1 -1
  11. package/templates/customization-ui-module-template/webpack.config.js +2 -2
  12. package/templates/platform-init-template/{local → {{platformName}}-core/local}/.env.example +1 -1
  13. package/templates/platform-init-template/{local → {{platformName}}-core/local}/platform-docker-compose.yml +6 -6
  14. package/templates/platform-init-template/{local/core-docker-compose.yml → {{platformName}}-core/local/{{platformName}}-core-docker-compose.yml} +7 -7
  15. package/templates/react-ui-module-template/Dockerfile +3 -3
  16. package/templates/react-ui-module-template/Dockerfile.development +1 -1
  17. package/templates/react-ui-module-template/package.json +3 -3
  18. package/templates/react-ui-module-template/src/Icon.tsx +1 -1
  19. package/templates/platform-init-template/local/docker-compose.yml +0 -3
  20. package/templates/platform-init-template/local/package.json +0 -18
  21. package/templates/platform-init-template/local/scripts/build.sh +0 -18
  22. package/templates/platform-init-template/local/scripts/install.sh +0 -18
  23. /package/templates/customization-ui-module-template/src/{platform-customization-ui.tsx → {{platformName}}-customization-ui.tsx} +0 -0
  24. /package/templates/platform-init-template/{core → {{platformName}}-core}/.changeset/config.json +0 -0
  25. /package/templates/platform-init-template/{core → {{platformName}}-core}/.nvmrc +0 -0
  26. /package/templates/platform-init-template/{core → {{platformName}}-core}/.syncpackrc +0 -0
  27. /package/templates/platform-init-template/{local → {{platformName}}-core/local}/environment/pae-nestjs-gateway-service.env +0 -0
  28. /package/templates/platform-init-template/{local → {{platformName}}-core/local}/nginx.conf +0 -0
  29. /package/templates/platform-init-template/{local → {{platformName}}-core/local}/ssl/cert.pem +0 -0
  30. /package/templates/platform-init-template/{local → {{platformName}}-core/local}/ssl/key.pem +0 -0
  31. /package/templates/platform-init-template/{core → {{platformName}}-core}/package.json +0 -0
  32. /package/templates/platform-init-template/{core → {{platformName}}-core}/packages-versions.json +0 -0
  33. /package/templates/platform-init-template/{core → {{platformName}}-core}/scripts/preinstall.mjs +0 -0
  34. /package/templates/platform-init-template/{core → {{platformName}}-core}/services/.gitkeep +0 -0
  35. /package/templates/platform-init-template/{core → {{platformName}}-core}/turbo.json +0 -0
  36. /package/templates/platform-init-template/{core → {{platformName}}-core}/ui/.gitkeep +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 Box4, Text as Text6, 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";
@@ -54,53 +54,76 @@ var CommandPalette = React.memo(function CommandPalette2({ commands, selectedInd
54
54
  });
55
55
 
56
56
  // src/components/scrollback-history.tsx
57
- import { Static, Text as Text4 } from "ink";
57
+ import { Static, Text as Text5 } from "ink";
58
58
 
59
59
  // src/components/welcome-banner.tsx
60
+ import React3 from "react";
61
+ import { Box as Box3, Text as Text4 } from "ink";
62
+
63
+ // src/components/working-directory.tsx
60
64
  import React2 from "react";
61
- 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";
62
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
+ }
73
+ var WorkingDirectory = React2.memo(function WorkingDirectory2() {
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
80
+ ] });
81
+ });
82
+
83
+ // src/components/welcome-banner.tsx
84
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
63
85
  var ASCII_ART = [
64
86
  "|---. .---|",
65
87
  "| \\/ |",
66
88
  "| /\\ |",
67
89
  "|---' '---|"
68
90
  ];
69
- var WelcomeBanner = React2.memo(function WelcomeBanner2({ version: version2 }) {
70
- return /* @__PURE__ */ jsxs3(Box3, { borderStyle: "round", flexDirection: "column", paddingX: 1, paddingY: 1, children: [
71
- /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "cyan", children: [
91
+ var WelcomeBanner = React3.memo(function WelcomeBanner2({ version: version2 }) {
92
+ return /* @__PURE__ */ jsxs4(Box3, { borderStyle: "round", flexDirection: "column", paddingX: 1, paddingY: 1, children: [
93
+ /* @__PURE__ */ jsxs4(Text4, { bold: true, color: "cyan", children: [
72
94
  "Blue Alba Platform CLI v",
73
95
  version2
74
96
  ] }),
75
- /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
76
- /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginRight: 2, children: ASCII_ART.map((line, i) => /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: line }, i)) }),
77
- /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", justifyContent: "center", marginRight: 2, children: [
78
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Welcome to the" }),
79
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Blue Alba Platform!" })
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: [
100
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Welcome to the" }),
101
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Blue Alba Platform!" })
80
102
  ] }),
81
- /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: ASCII_ART.map((_, i) => /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2502" }, i)) }),
82
- /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginLeft: 2, children: [
83
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: "Tips for getting started" }),
84
- /* @__PURE__ */ jsxs3(Text3, { 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: [
105
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "Tips for getting started" }),
106
+ /* @__PURE__ */ jsxs4(Text4, { children: [
85
107
  "Run ",
86
- /* @__PURE__ */ jsx3(Text3, { color: "green", children: "/init" }),
108
+ /* @__PURE__ */ jsx4(Text4, { color: "green", children: "/init" }),
87
109
  " to start building a platform"
88
110
  ] })
89
111
  ] })
90
- ] })
112
+ ] }),
113
+ /* @__PURE__ */ jsx4(WorkingDirectory, {})
91
114
  ] });
92
115
  });
93
116
 
94
117
  // src/components/scrollback-history.tsx
95
- import { jsx as jsx4 } from "react/jsx-runtime";
118
+ import { jsx as jsx5 } from "react/jsx-runtime";
96
119
  function ScrollbackHistory({ items, version: version2 }) {
97
- return /* @__PURE__ */ jsx4(Static, { items, children: (item) => item.kind === "banner" ? /* @__PURE__ */ jsx4(WelcomeBanner, { version: version2 }, item.id) : /* @__PURE__ */ jsx4(Text4, { children: item.line }, item.id) });
120
+ return /* @__PURE__ */ jsx5(Static, { items, children: (item) => item.kind === "banner" ? /* @__PURE__ */ jsx5(WelcomeBanner, { version: version2 }, item.id) : /* @__PURE__ */ jsx5(Text5, { children: item.line }, item.id) });
98
121
  }
99
122
 
100
123
  // src/components/spinner.tsx
101
124
  import { useState, useEffect } from "react";
102
- import { Text as Text5 } from "ink";
103
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
125
+ import { Text as Text6 } from "ink";
126
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
104
127
  var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
105
128
  var INTERVAL_MS = 80;
106
129
  function Spinner({ label }) {
@@ -111,12 +134,69 @@ function Spinner({ label }) {
111
134
  }, INTERVAL_MS);
112
135
  return () => clearInterval(id);
113
136
  }, []);
114
- return /* @__PURE__ */ jsxs4(Text5, { children: [
115
- /* @__PURE__ */ jsxs4(Text5, { color: "cyan", children: [
137
+ return /* @__PURE__ */ jsxs5(Text6, { children: [
138
+ /* @__PURE__ */ jsxs5(Text6, { color: "cyan", children: [
116
139
  FRAMES[frame],
117
140
  " "
118
141
  ] }),
119
- label && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: label })
142
+ label && /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: label })
143
+ ] });
144
+ }
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)" })
120
200
  ] });
121
201
  }
122
202
 
@@ -124,8 +204,7 @@ function Spinner({ label }) {
124
204
  import { Fzf } from "fzf";
125
205
 
126
206
  // src/commands/create-application/create-application.command.ts
127
- import { join as join10 } from "path";
128
- import { cwd as cwd2 } from "process";
207
+ import { join as join11, resolve } from "path";
129
208
  import { mkdir as mkdir2 } from "fs/promises";
130
209
 
131
210
  // src/commands/create-application/scaffold-application-monorepo.ts
@@ -232,7 +311,12 @@ async function scaffoldBootstrap(bootstrapServiceDir, organizationName, bootstra
232
311
  {
233
312
  templateDir: bootstrapTemplateDir,
234
313
  outputDir: bootstrapServiceDir,
235
- variables: { organizationName, bootstrapName, bootstrapServiceDir: bootstrapServiceDir_var },
314
+ variables: {
315
+ organizationName,
316
+ bootstrapName,
317
+ bootstrapServiceName: `${bootstrapName}-bootstrap-service`,
318
+ bootstrapServiceDir: bootstrapServiceDir_var
319
+ },
236
320
  exclude: ["src/data/shared-libraries.json", "src/data/platform/modules-config.json"]
237
321
  },
238
322
  (message) => logger.log(message)
@@ -290,8 +374,8 @@ var nestjsServiceModuleTemplateDir = join6(
290
374
  "templates",
291
375
  "nestjs-service-module-template"
292
376
  );
293
- async function scaffoldNestjsService(serviceDir, organizationName, applicationName, applicationDisplayName, serviceBaseDir, logger) {
294
- const serviceName = `${applicationName}-service`;
377
+ async function scaffoldNestjsService(serviceDir, organizationName, platformName, applicationName, applicationDisplayName, serviceBaseDir, logger) {
378
+ const serviceName = `${platformName}-${applicationName}-service`;
295
379
  const serviceDisplayName = `${applicationDisplayName} Service`;
296
380
  logger.log(`Creating NestJS service "${serviceName}"...`);
297
381
  await applyTemplate(
@@ -322,33 +406,35 @@ async function addModuleEntry(bootstrapServiceDir, applicationName, entry, logge
322
406
  }
323
407
 
324
408
  // src/commands/create-application/module-entry-builders.ts
325
- function buildServiceModuleEntry(organizationName, applicationName, applicationDisplayName) {
409
+ function buildServiceModuleEntry(organizationName, platformName, applicationName, applicationDisplayName) {
410
+ const serviceName = `${platformName}-${applicationName}-service`;
326
411
  return {
327
- name: `@${organizationName}/${applicationName}-service`,
412
+ name: `@${organizationName}/${serviceName}`,
328
413
  displayName: `${applicationDisplayName} Service`,
329
414
  type: "service",
330
- baseUrl: `/${applicationName}-service`,
415
+ baseUrl: `/${serviceName}`,
331
416
  service: {
332
- host: `${applicationName}-service`,
417
+ host: serviceName,
333
418
  port: 80
334
419
  },
335
420
  dependsOn: []
336
421
  };
337
422
  }
338
- function buildUiModuleEntry(organizationName, applicationName, applicationDisplayName) {
423
+ function buildUiModuleEntry(organizationName, platformName, applicationName, applicationDisplayName, uiNameOverride) {
424
+ const uiName = uiNameOverride ?? `${platformName}-${applicationName}-ui`;
339
425
  return {
340
- name: `@${organizationName}/${applicationName}-ui`,
426
+ name: `@${organizationName}/${uiName}`,
341
427
  displayName: applicationDisplayName,
342
428
  type: "app",
343
- baseUrl: `/${applicationName}-ui`,
429
+ baseUrl: `/${uiName}`,
344
430
  service: {
345
- host: `${applicationName}-ui`,
431
+ host: uiName,
346
432
  port: 80
347
433
  },
348
434
  ui: {
349
435
  route: `/${applicationName}`,
350
436
  mountAtSelector: "#pae-shell-ui-content",
351
- bundleFile: `${organizationName}-${applicationName}-ui.js`,
437
+ bundleFile: `${organizationName}-${uiName}.js`,
352
438
  customProps: {}
353
439
  },
354
440
  dependsOn: [`@bluealba/pae-shell-ui`]
@@ -357,10 +443,11 @@ function buildUiModuleEntry(organizationName, applicationName, applicationDispla
357
443
 
358
444
  // src/commands/create-application/create-docker-compose.ts
359
445
  import { writeFile as writeFile3 } from "fs/promises";
360
- function buildBootstrapBlock(applicationName) {
361
- return ` ${applicationName}-bootstrap-service:
446
+ function buildBootstrapBlock(platformName, applicationName) {
447
+ const bootstrapName = `${platformName}-${applicationName}-bootstrap-service`;
448
+ return ` ${bootstrapName}:
362
449
  build:
363
- context: ../${applicationName}/services/${applicationName}-bootstrap-service
450
+ context: ../../${platformName}-${applicationName}/services/${bootstrapName}
364
451
  dockerfile: Dockerfile.development
365
452
  environment:
366
453
  - NODE_TLS_REJECT_UNAUTHORIZED=0
@@ -370,27 +457,29 @@ function buildBootstrapBlock(applicationName) {
370
457
  - GATEWAY_SERVICE_URL=\${PAE_GATEWAY_URL}
371
458
  - SERVICE_ACCESS_SECRET=\${PAE_GATEWAY_SERVICE_ACCESS_SECRET}
372
459
  volumes:
373
- - \${PWD}/../:/app/out
460
+ - \${PWD}/:/app/out
374
461
  depends_on:
375
462
  pae-nestjs-gateway-service:
376
463
  condition: service_healthy
377
464
  `;
378
465
  }
379
- function buildUiBlock(applicationName, uiPort) {
380
- return ` ${applicationName}-ui:
466
+ function buildUiBlock(platformName, applicationName, uiPort) {
467
+ const uiName = `${platformName}-${applicationName}-ui`;
468
+ return ` ${uiName}:
381
469
  build:
382
- context: ../${applicationName}/ui/${applicationName}-ui
470
+ context: ../../${platformName}-${applicationName}/ui/${uiName}
383
471
  dockerfile: Dockerfile.development
384
472
  ports:
385
473
  - ${uiPort}:80
386
474
  volumes:
387
- - \${PWD}/../:/app/out
475
+ - \${PWD}/:/app/out
388
476
  `;
389
477
  }
390
- function buildBackendBlock(applicationName, servicePort) {
391
- return ` ${applicationName}-service:
478
+ function buildBackendBlock(platformName, applicationName, servicePort) {
479
+ const serviceName = `${platformName}-${applicationName}-service`;
480
+ return ` ${serviceName}:
392
481
  build:
393
- context: ../${applicationName}/services/${applicationName}-service
482
+ context: ../../${platformName}-${applicationName}/services/${serviceName}
394
483
  dockerfile: Dockerfile.development
395
484
  args:
396
485
  - BA_NPM_AUTH_TOKEN=$BA_NPM_AUTH_TOKEN
@@ -399,18 +488,18 @@ function buildBackendBlock(applicationName, servicePort) {
399
488
  environment:
400
489
  - GATEWAY_URL=\${PAE_GATEWAY_URL}
401
490
  volumes:
402
- - \${PWD}/../:/app/out
491
+ - \${PWD}/:/app/out
403
492
  `;
404
493
  }
405
- async function createDockerCompose(dockerComposePath, applicationName, hasUserInterface, hasBackendService, uiPort, servicePort, logger) {
406
- const blocks = ["services:\n", buildBootstrapBlock(applicationName)];
494
+ async function createDockerCompose(dockerComposePath, applicationName, platformName, hasUserInterface, hasBackendService, uiPort, servicePort, logger) {
495
+ const blocks = ["services:\n", buildBootstrapBlock(platformName, applicationName)];
407
496
  if (hasUserInterface) {
408
- blocks.push(buildUiBlock(applicationName, uiPort));
497
+ blocks.push(buildUiBlock(platformName, applicationName, uiPort));
409
498
  }
410
499
  if (hasBackendService) {
411
- blocks.push(buildBackendBlock(applicationName, servicePort));
500
+ blocks.push(buildBackendBlock(platformName, applicationName, servicePort));
412
501
  }
413
- const content = blocks.join("\n");
502
+ const content = blocks.join("\n") + "\n";
414
503
  try {
415
504
  await writeFile3(dockerComposePath, content, "utf-8");
416
505
  logger.log(`Created docker-compose: ${dockerComposePath}`);
@@ -420,32 +509,17 @@ async function createDockerCompose(dockerComposePath, applicationName, hasUserIn
420
509
  }
421
510
  }
422
511
 
423
- // src/commands/create-application/update-root-docker-compose.ts
424
- import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
425
- async function updateRootDockerCompose(rootDockerComposePath, applicationName, logger) {
426
- try {
427
- const existing = await readFile3(rootDockerComposePath, "utf-8");
428
- const includeLine = ` - path: ./${applicationName}-docker-compose.yml
429
- `;
430
- await writeFile4(rootDockerComposePath, existing + includeLine, "utf-8");
431
- logger.log(`Updated root docker-compose: ${rootDockerComposePath}`);
432
- } catch (err) {
433
- const message = err instanceof Error ? err.message : String(err);
434
- logger.log(`Warning: Could not update root docker-compose \u2014 ${message}`);
435
- }
436
- }
437
-
438
512
  // src/commands/create-application/port-allocator.ts
439
513
  import { join as join8 } from "path";
440
- import { readdir as readdir2, readFile as readFile4 } from "fs/promises";
514
+ import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
441
515
  async function getNextAvailablePort(localDir) {
442
516
  const allPorts = [];
443
517
  try {
444
518
  const files = await readdir2(localDir);
445
- const dockerComposeFiles = files.filter((f) => f.endsWith("-docker-compose.yml"));
446
- for (const file of dockerComposeFiles) {
519
+ const ymlFiles = files.filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
520
+ for (const file of ymlFiles) {
447
521
  try {
448
- const content = await readFile4(join8(localDir, file), "utf-8");
522
+ const content = await readFile3(join8(localDir, file), "utf-8");
449
523
  const regex = /^\s*-\s*"?(\d+):\d+"?\s*$/gm;
450
524
  let match;
451
525
  while ((match = regex.exec(content)) !== null) {
@@ -464,44 +538,130 @@ function formatError(err) {
464
538
  return err instanceof Error ? err.message : String(err);
465
539
  }
466
540
 
467
- // src/utils/platform-check.ts
468
- import { access } from "fs/promises";
469
- import { join as join9 } from "path";
470
- import { cwd } from "process";
471
- async function isPlatformInitialized() {
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;
472
547
  try {
473
- await access(join9(cwd(), "core"));
474
- await access(join9(cwd(), "local"));
475
- return true;
548
+ entries = await readdir3(dir, { withFileTypes: true });
476
549
  } catch {
477
- return false;
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
+ }
569
+ }
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;
478
590
  }
479
591
  }
480
592
 
593
+ // src/utils/platform-check.ts
594
+ async function isPlatformInitialized() {
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
+ };
630
+ }
631
+
481
632
  // src/commands/create-application/create-application.command.ts
482
633
  var CREATE_APPLICATION_COMMAND_NAME = "create-application";
483
634
  var createApplicationCommand = {
484
635
  name: CREATE_APPLICATION_COMMAND_NAME,
485
- description: "Create an application in a platform"
636
+ description: "Create an application in a platform",
637
+ hidden: (ctx) => !ctx.platformInitialized
486
638
  };
487
639
  async function createApplication(params, logger) {
488
640
  const {
489
- organizationName,
490
- platformName,
491
641
  applicationName,
492
642
  applicationDisplayName,
493
643
  applicationDescription,
494
644
  hasUserInterface,
495
645
  hasBackendService
496
646
  } = params;
497
- if (!await isPlatformInitialized()) {
647
+ const layout = await findPlatformLayout();
648
+ if (!layout) {
498
649
  logger.log("Error: Cannot create an application \u2014 no platform initialized in this directory.");
499
650
  return;
500
651
  }
501
- const rootDir = cwd2();
502
- const applicationDir = join10(rootDir, applicationName);
503
- const bootstrapServiceDir = join10(applicationDir, "services", `${applicationName}-bootstrap-service`);
504
- const localDir = join10(rootDir, "local");
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);
505
665
  logger.log(`Creating application monorepo "${applicationName}"...`);
506
666
  try {
507
667
  await scaffoldApplicationMonorepo(applicationDir, organizationName, platformName, applicationName, logger);
@@ -509,12 +669,12 @@ async function createApplication(params, logger) {
509
669
  logger.log(`Error: Could not scaffold application monorepo \u2014 ${formatError(err)}`);
510
670
  return;
511
671
  }
512
- await mkdir2(join10(applicationDir, "services"), { recursive: true });
513
- await mkdir2(join10(applicationDir, "ui"), { recursive: true });
514
- logger.log(`Creating bootstrap service "${applicationName}-bootstrap-service"...`);
515
- const bootstrapServiceDir_var = `${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`;
516
676
  try {
517
- await scaffoldBootstrap(bootstrapServiceDir, organizationName, applicationName, bootstrapServiceDir_var, logger);
677
+ await scaffoldBootstrap(bootstrapServiceDir, organizationName, `${platformName}-${applicationName}`, bootstrapServiceBaseDir, logger);
518
678
  } catch (err) {
519
679
  logger.log(`Error: Could not scaffold bootstrap service \u2014 ${formatError(err)}`);
520
680
  return;
@@ -530,7 +690,7 @@ async function createApplication(params, logger) {
530
690
  await addModuleEntry(
531
691
  bootstrapServiceDir,
532
692
  applicationName,
533
- buildUiModuleEntry(organizationName, applicationName, applicationDisplayName),
693
+ buildUiModuleEntry(organizationName, platformName, applicationName, applicationDisplayName),
534
694
  logger
535
695
  );
536
696
  }
@@ -538,13 +698,14 @@ async function createApplication(params, logger) {
538
698
  await addModuleEntry(
539
699
  bootstrapServiceDir,
540
700
  applicationName,
541
- buildServiceModuleEntry(organizationName, applicationName, applicationDisplayName),
701
+ buildServiceModuleEntry(organizationName, platformName, applicationName, applicationDisplayName),
542
702
  logger
543
703
  );
544
704
  }
545
705
  if (hasUserInterface) {
546
- const uiDir = join10(applicationDir, "ui", `${applicationName}-ui`);
547
- const uiBaseDir = `${applicationName}/ui`;
706
+ const uiName = `${platformName}-${applicationName}-ui`;
707
+ const uiDir = join11(applicationDir, "ui", uiName);
708
+ const uiBaseDir = `${platformName}-${applicationName}/ui`;
548
709
  try {
549
710
  await scaffoldUiModule(uiDir, organizationName, platformName, applicationName, applicationDisplayName, uiBaseDir, logger);
550
711
  } catch (err) {
@@ -553,36 +714,49 @@ async function createApplication(params, logger) {
553
714
  }
554
715
  }
555
716
  if (hasBackendService) {
556
- const serviceDir = join10(applicationDir, "services", `${applicationName}-service`);
557
- const serviceBaseDir = `${applicationName}/services`;
717
+ const serviceName = `${platformName}-${applicationName}-service`;
718
+ const serviceDir = join11(applicationDir, "services", serviceName);
719
+ const serviceBaseDir = `${platformName}-${applicationName}/services`;
558
720
  try {
559
- await scaffoldNestjsService(serviceDir, organizationName, applicationName, applicationDisplayName, serviceBaseDir, logger);
721
+ await scaffoldNestjsService(serviceDir, organizationName, platformName, applicationName, applicationDisplayName, serviceBaseDir, logger);
560
722
  } catch (err) {
561
723
  logger.log(`Error: Could not scaffold NestJS service \u2014 ${formatError(err)}`);
562
724
  return;
563
725
  }
564
726
  }
565
- const dockerComposePath = join10(localDir, `${applicationName}-docker-compose.yml`);
727
+ const dockerComposePath = join11(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
566
728
  const basePort = await getNextAvailablePort(localDir);
567
729
  const uiPort = hasUserInterface ? basePort : 0;
568
730
  const servicePort = hasBackendService ? hasUserInterface ? basePort + 1 : basePort : 0;
569
731
  await createDockerCompose(
570
732
  dockerComposePath,
571
733
  applicationName,
734
+ platformName,
572
735
  hasUserInterface,
573
736
  hasBackendService,
574
737
  uiPort,
575
738
  servicePort,
576
739
  logger
577
740
  );
578
- const rootDockerComposePath = join10(localDir, "docker-compose.yml");
579
- await updateRootDockerCompose(rootDockerComposePath, applicationName, logger);
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
+ }
580
754
  logger.log(`Done! Application "${applicationName}" created at ${applicationDir}`);
581
755
  }
582
756
 
583
757
  // src/commands/init/init.command.ts
584
- import { join as join16 } from "path";
585
- import { cwd as cwd3 } from "process";
758
+ import { join as join17 } from "path";
759
+ import { cwd as cwd4 } from "process";
586
760
 
587
761
  // src/utils/string.ts
588
762
  function camelize(name) {
@@ -591,9 +765,9 @@ function camelize(name) {
591
765
 
592
766
  // src/commands/init/scaffold-platform.ts
593
767
  import { fileURLToPath as fileURLToPath6 } from "url";
594
- import { join as join11, dirname as dirname7 } from "path";
595
- var templateDir = join11(
596
- 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)),
597
771
  "..",
598
772
  "templates",
599
773
  "platform-init-template"
@@ -604,9 +778,9 @@ async function scaffoldPlatform(outputDir, variables, logger) {
604
778
 
605
779
  // src/commands/init/scaffold-platform-bootstrap.ts
606
780
  import { fileURLToPath as fileURLToPath7 } from "url";
607
- import { join as join12, dirname as dirname8 } from "path";
608
- var templateDir2 = join12(
609
- 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)),
610
784
  "..",
611
785
  "templates",
612
786
  "bootstrap-service-template"
@@ -617,9 +791,9 @@ async function scaffoldPlatformBootstrap(outputDir, variables, logger) {
617
791
 
618
792
  // src/commands/init/scaffold-customization-ui.ts
619
793
  import { fileURLToPath as fileURLToPath8 } from "url";
620
- import { join as join13, dirname as dirname9 } from "path";
621
- var templateDir3 = join13(
622
- 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)),
623
797
  "..",
624
798
  "templates",
625
799
  "customization-ui-module-template"
@@ -629,19 +803,20 @@ async function scaffoldCustomizationUi(outputDir, variables, logger) {
629
803
  }
630
804
 
631
805
  // src/commands/init/register-customization-module.ts
632
- import { join as join14 } from "path";
806
+ import { join as join15 } from "path";
633
807
  import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
634
- async function registerCustomizationModule(bootstrapServiceDir, organizationName, logger) {
635
- const modulesJsonPath = join14(bootstrapServiceDir, "src", "data", "platform", "modules.json");
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`;
636
811
  const entry = {
637
- name: `@${organizationName}/platform-customization-ui`,
812
+ name: `@${organizationName}/${customizationUiName}`,
638
813
  displayName: "Platform Customization UI",
639
814
  type: "app",
640
- baseUrl: "/platform-customization-ui",
641
- service: { host: "platform-customization-ui", port: 80 },
815
+ baseUrl: `/${customizationUiName}`,
816
+ service: { host: customizationUiName, port: 80 },
642
817
  ui: {
643
818
  route: "/",
644
- bundleFile: "platform-customization-ui.js",
819
+ bundleFile: `${customizationUiName}.js`,
645
820
  isPlatformCustomization: true,
646
821
  customProps: {}
647
822
  },
@@ -655,7 +830,7 @@ async function registerCustomizationModule(bootstrapServiceDir, organizationName
655
830
 
656
831
  // src/commands/init/generate-local-env.ts
657
832
  import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
658
- import { join as join15 } from "path";
833
+ import { join as join16 } from "path";
659
834
 
660
835
  // src/utils/random.ts
661
836
  import { randomBytes } from "crypto";
@@ -664,10 +839,10 @@ function generateRandomSecret() {
664
839
  }
665
840
 
666
841
  // src/commands/init/generate-local-env.ts
667
- async function generateLocalEnv(outputDir, logger) {
668
- const examplePath = join15(outputDir, "local", ".env.example");
669
- const envPath = join15(outputDir, "local", ".env");
670
- logger.log("Generating local/.env with random secrets...");
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...`);
671
846
  const content = await readFile6(examplePath, "utf-8");
672
847
  const result = content.replace(/^(PAE_AUTH_JWT_SECRET|PAE_GATEWAY_SERVICE_ACCESS_SECRET|PAE_DB_PASSWORD)=$/gm, (_, key) => {
673
848
  return `${key}=${generateRandomSecret()}`;
@@ -679,7 +854,8 @@ async function generateLocalEnv(outputDir, logger) {
679
854
  var INIT_COMMAND_NAME = "init";
680
855
  var initCommand = {
681
856
  name: INIT_COMMAND_NAME,
682
- description: "Initialize a new platform"
857
+ description: "Initialize a new platform",
858
+ hidden: ({ platformInitialized }) => platformInitialized
683
859
  };
684
860
  async function init(params, logger) {
685
861
  if (await isPlatformInitialized()) {
@@ -688,15 +864,22 @@ async function init(params, logger) {
688
864
  }
689
865
  const { organizationName, platformName, platformDisplayName } = params;
690
866
  const platformTitle = camelize(platformName);
691
- const outputDir = cwd3();
867
+ const outputDir = cwd4();
692
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}`;
693
874
  const variables = {
694
875
  organizationName,
695
876
  platformName,
696
877
  platformTitle,
697
878
  platformDisplayName,
698
- bootstrapName: "platform",
699
- bootstrapServiceDir: "core/services"
879
+ bootstrapName: platformName,
880
+ bootstrapServiceName,
881
+ customizationUiName,
882
+ bootstrapServiceDir: `${coreDirName}/services`
700
883
  };
701
884
  try {
702
885
  await scaffoldPlatform(outputDir, variables, logger);
@@ -705,14 +888,22 @@ async function init(params, logger) {
705
888
  return;
706
889
  }
707
890
  try {
708
- await generateLocalEnv(outputDir, 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`);
709
900
  } catch (err) {
710
- logger.log(`Error: Could not generate local/.env \u2014 ${formatError(err)}`);
901
+ logger.log(`Error: Could not write product manifest \u2014 ${formatError(err)}`);
711
902
  return;
712
903
  }
713
904
  try {
714
905
  await scaffoldPlatformBootstrap(
715
- join16(outputDir, "core", "services", "platform-bootstrap-service"),
906
+ join17(outputDir, coreDirName, "services", bootstrapServiceName),
716
907
  variables,
717
908
  logger
718
909
  );
@@ -722,7 +913,7 @@ async function init(params, logger) {
722
913
  }
723
914
  try {
724
915
  await scaffoldCustomizationUi(
725
- join16(outputDir, "core", "ui", "platform-customization-ui"),
916
+ join17(outputDir, coreDirName, "ui", customizationUiName),
726
917
  variables,
727
918
  logger
728
919
  );
@@ -732,9 +923,10 @@ async function init(params, logger) {
732
923
  }
733
924
  try {
734
925
  await registerCustomizationModule(
735
- join16(outputDir, "core", "services", "platform-bootstrap-service"),
926
+ join17(outputDir, coreDirName, "services", bootstrapServiceName),
736
927
  organizationName,
737
- logger
928
+ logger,
929
+ platformName
738
930
  );
739
931
  } catch (err) {
740
932
  logger.log(`Error: Could not register customization module \u2014 ${formatError(err)}`);
@@ -744,8 +936,7 @@ async function init(params, logger) {
744
936
  }
745
937
 
746
938
  // src/commands/configure-idp/configure-idp.command.ts
747
- import { join as join17 } from "path";
748
- import { cwd as cwd4 } from "process";
939
+ import { join as join18 } from "path";
749
940
  import { fetch as undiciFetch, Agent } from "undici";
750
941
 
751
942
  // src/utils/env-reader.ts
@@ -820,14 +1011,16 @@ function getAllProviders() {
820
1011
  var CONFIGURE_IDP_COMMAND_NAME = "configure-idp";
821
1012
  var configureIdpCommand = {
822
1013
  name: CONFIGURE_IDP_COMMAND_NAME,
823
- description: "Configure an Identity Provider (IDP) in the gateway"
1014
+ description: "Configure an Identity Provider (IDP) in the gateway",
1015
+ hidden: (ctx) => !ctx.platformInitialized
824
1016
  };
825
1017
  async function configureIdp(params, logger) {
826
- if (!await isPlatformInitialized()) {
1018
+ const layout = await findPlatformLayout();
1019
+ if (!layout) {
827
1020
  logger.log("Error: Cannot configure an IDP \u2014 no platform initialized in this directory.");
828
1021
  return;
829
1022
  }
830
- const envPath = join17(cwd4(), "local", ".env");
1023
+ const envPath = join18(layout.localDir, ".env");
831
1024
  let env;
832
1025
  try {
833
1026
  env = await readEnvFile(envPath);
@@ -838,11 +1031,11 @@ async function configureIdp(params, logger) {
838
1031
  const gatewayUrl = env.get("PAE_GATEWAY_HOST_URL");
839
1032
  const accessSecret = env.get("PAE_GATEWAY_SERVICE_ACCESS_SECRET");
840
1033
  if (!gatewayUrl) {
841
- logger.log("Error: PAE_GATEWAY_HOST_URL is not set in local/.env");
1034
+ logger.log("Error: PAE_GATEWAY_HOST_URL is not set in core/local/.env");
842
1035
  return;
843
1036
  }
844
1037
  if (!accessSecret) {
845
- logger.log("Error: PAE_GATEWAY_SERVICE_ACCESS_SECRET is not set in local/.env");
1038
+ logger.log("Error: PAE_GATEWAY_SERVICE_ACCESS_SECRET is not set in core/local/.env");
846
1039
  return;
847
1040
  }
848
1041
  const provider = idpProviderRegistry.get(params.providerType);
@@ -879,15 +1072,14 @@ async function configureIdp(params, logger) {
879
1072
  }
880
1073
 
881
1074
  // src/commands/create-service-module/create-service-module.command.ts
882
- import { join as join19 } from "path";
883
- import { cwd as cwd5 } from "process";
884
- import { access as access2, readFile as readFile9 } from "fs/promises";
1075
+ import { join as join20, resolve as resolve2 } from "path";
1076
+ import { access as access3 } from "fs/promises";
885
1077
 
886
1078
  // src/commands/create-service-module/scaffold-service-module.ts
887
1079
  import { fileURLToPath as fileURLToPath9 } from "url";
888
- import { join as join18, dirname as dirname10 } from "path";
889
- var nestjsServiceModuleTemplateDir2 = join18(
890
- 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)),
891
1083
  "..",
892
1084
  "templates",
893
1085
  "nestjs-service-module-template"
@@ -922,11 +1114,11 @@ function buildCustomServiceModuleEntry(organizationName, serviceName, serviceDis
922
1114
 
923
1115
  // src/commands/create-service-module/append-docker-compose.ts
924
1116
  import { readFile as readFile8, writeFile as writeFile7 } from "fs/promises";
925
- async function appendServiceToDockerCompose(dockerComposePath, serviceName, applicationName, port, logger) {
1117
+ async function appendServiceToDockerCompose(dockerComposePath, serviceName, platformName, applicationName, port, logger) {
926
1118
  const block = `
927
1119
  ${serviceName}:
928
1120
  build:
929
- context: ../${applicationName}/services/${serviceName}
1121
+ context: ../../${platformName}-${applicationName}/services/${serviceName}
930
1122
  dockerfile: Dockerfile.development
931
1123
  args:
932
1124
  - BA_NPM_AUTH_TOKEN=$BA_NPM_AUTH_TOKEN
@@ -935,7 +1127,7 @@ async function appendServiceToDockerCompose(dockerComposePath, serviceName, appl
935
1127
  environment:
936
1128
  - GATEWAY_URL=\${PAE_GATEWAY_URL}
937
1129
  volumes:
938
- - \${PWD}/../:/app/out
1130
+ - \${PWD}/:/app/out
939
1131
  `;
940
1132
  try {
941
1133
  const existing = await readFile8(dockerComposePath, "utf-8");
@@ -951,45 +1143,47 @@ async function appendServiceToDockerCompose(dockerComposePath, serviceName, appl
951
1143
  var CREATE_SERVICE_MODULE_COMMAND_NAME = "create-service-module";
952
1144
  var createServiceModuleCommand = {
953
1145
  name: CREATE_SERVICE_MODULE_COMMAND_NAME,
954
- 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
955
1148
  };
956
1149
  async function createServiceModule(params, logger) {
957
- const { applicationName, serviceName, serviceDisplayName } = params;
958
- if (!await isPlatformInitialized()) {
1150
+ const { applicationName, serviceName, serviceDisplayName, serviceNameSuffix } = params;
1151
+ const layout = await findPlatformLayout();
1152
+ if (!layout) {
959
1153
  logger.log("Error: Cannot create a service module \u2014 no platform initialized in this directory.");
960
1154
  return;
961
1155
  }
962
- const rootDir = cwd5();
963
- const applicationDir = join19(rootDir, applicationName);
1156
+ const { rootDir, coreDirName, localDir } = layout;
1157
+ let manifest;
1158
+ try {
1159
+ manifest = await readManifest(rootDir, coreDirName);
1160
+ } catch (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.`);
1168
+ return;
1169
+ }
1170
+ const applicationDir = resolve2(join20(rootDir, coreDirName), appEntry.localPath);
964
1171
  try {
965
- await access2(applicationDir);
1172
+ await access3(applicationDir);
966
1173
  } catch {
967
1174
  logger.log(`Error: The specified application "${applicationName}" does not exist in the platform.`);
968
1175
  return;
969
1176
  }
970
- const fullServiceName = `${serviceName}-service`;
971
- 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);
972
1180
  try {
973
- await access2(serviceDir);
1181
+ await access3(serviceDir);
974
1182
  logger.log(`Error: A service named "${fullServiceName}" already exists in application "${applicationName}".`);
975
1183
  return;
976
1184
  } catch {
977
1185
  }
978
- let organizationName;
979
- try {
980
- const corePackageJson = JSON.parse(
981
- await readFile9(join19(rootDir, "core", "package.json"), "utf-8")
982
- );
983
- const scopeMatch = corePackageJson.name.match(/^@([^/]+)\//);
984
- organizationName = scopeMatch?.[1];
985
- if (!organizationName) {
986
- throw new Error(`Could not parse organization from package name: "${corePackageJson.name}"`);
987
- }
988
- } catch (err) {
989
- logger.log(`Error: Could not read core/package.json \u2014 ${formatError(err)}`);
990
- return;
991
- }
992
- const serviceBaseDir = `${applicationName}/services`;
1186
+ const serviceBaseDir = `${platformName}-${applicationName}/services`;
993
1187
  try {
994
1188
  await scaffoldServiceModule(
995
1189
  serviceDir,
@@ -1003,36 +1197,35 @@ async function createServiceModule(params, logger) {
1003
1197
  logger.log(`Error: Could not scaffold NestJS service \u2014 ${formatError(err)}`);
1004
1198
  return;
1005
1199
  }
1006
- const bootstrapServiceDir = join19(applicationDir, "services", `${applicationName}-bootstrap-service`);
1200
+ const bootstrapServiceDir = join20(applicationDir, "services", `${platformName}-${applicationName}-bootstrap-service`);
1007
1201
  const moduleEntry = buildCustomServiceModuleEntry(organizationName, fullServiceName, serviceDisplayName);
1008
1202
  await addModuleEntry(bootstrapServiceDir, applicationName, moduleEntry, logger);
1009
- const localDir = join19(rootDir, "local");
1010
- const dockerComposePath = join19(localDir, `${applicationName}-docker-compose.yml`);
1203
+ const dockerComposePath = join20(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
1011
1204
  const port = await getNextAvailablePort(localDir);
1012
- await appendServiceToDockerCompose(dockerComposePath, fullServiceName, applicationName, port, logger);
1205
+ await appendServiceToDockerCompose(dockerComposePath, fullServiceName, platformName, applicationName, port, logger);
1013
1206
  logger.log(`Done! Service module "${fullServiceName}" added to application "${applicationName}".`);
1014
1207
  }
1015
1208
 
1016
1209
  // src/commands/create-ui-module/create-ui-module.command.ts
1017
- import { join as join20 } from "path";
1018
- import { cwd as cwd6 } from "process";
1019
- import { access as access3, readdir as readdir3, readFile as readFile11 } from "fs/promises";
1210
+ import { join as join21, resolve as resolve3 } from "path";
1211
+ import { access as access4, readdir as readdir4 } from "fs/promises";
1020
1212
 
1021
1213
  // src/commands/create-ui-module/append-ui-docker-compose.ts
1022
- import { readFile as readFile10, writeFile as writeFile8 } from "fs/promises";
1023
- async function appendUiToDockerCompose(dockerComposePath, 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`;
1024
1217
  const block = `
1025
- ${applicationName}-ui:
1218
+ ${uiName}:
1026
1219
  build:
1027
- context: ../${applicationName}/ui/${applicationName}-ui
1220
+ context: ../../${platformName}-${applicationName}/ui/${uiName}
1028
1221
  dockerfile: Dockerfile.development
1029
1222
  ports:
1030
1223
  - ${port}:80
1031
1224
  volumes:
1032
- - \${PWD}/../:/app/out
1225
+ - \${PWD}/:/app/out
1033
1226
  `;
1034
1227
  try {
1035
- const existing = await readFile10(dockerComposePath, "utf-8");
1228
+ const existing = await readFile9(dockerComposePath, "utf-8");
1036
1229
  await writeFile8(dockerComposePath, existing + block, "utf-8");
1037
1230
  logger.log(`Updated docker-compose: ${dockerComposePath}`);
1038
1231
  } catch (err) {
@@ -1045,25 +1238,40 @@ async function appendUiToDockerCompose(dockerComposePath, applicationName, port,
1045
1238
  var CREATE_UI_MODULE_COMMAND_NAME = "create-ui-module";
1046
1239
  var createUiModuleCommand = {
1047
1240
  name: CREATE_UI_MODULE_COMMAND_NAME,
1048
- description: "Add a UI module to an existing application"
1241
+ description: "Add a UI module to an existing application",
1242
+ hidden: (ctx) => !ctx.platformInitialized
1049
1243
  };
1050
1244
  async function createUiModule(params, logger) {
1051
- const { applicationName, applicationDisplayName } = params;
1052
- if (!await isPlatformInitialized()) {
1245
+ const { applicationName, applicationDisplayName, uiModuleSuffix } = params;
1246
+ const layout = await findPlatformLayout();
1247
+ if (!layout) {
1053
1248
  logger.log("Error: Cannot create a UI module \u2014 no platform initialized in this directory.");
1054
1249
  return;
1055
1250
  }
1056
- const rootDir = cwd6();
1057
- const applicationDir = join20(rootDir, applicationName);
1251
+ const { rootDir, coreDirName, localDir } = layout;
1252
+ let manifest;
1058
1253
  try {
1059
- await access3(applicationDir);
1254
+ manifest = await readManifest(rootDir, coreDirName);
1255
+ } catch (err) {
1256
+ logger.log(`Error: Could not read product manifest \u2014 ${formatError(err)}`);
1257
+ return;
1258
+ }
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);
1266
+ try {
1267
+ await access4(applicationDir);
1060
1268
  } catch {
1061
1269
  logger.log(`Error: The specified application "${applicationName}" does not exist in the platform.`);
1062
1270
  return;
1063
1271
  }
1064
- const uiDir = join20(applicationDir, "ui");
1272
+ const uiDir = join21(applicationDir, "ui");
1065
1273
  try {
1066
- const uiEntries = await readdir3(uiDir);
1274
+ const uiEntries = await readdir4(uiDir);
1067
1275
  const existingUiModules = uiEntries.filter((e) => e !== ".gitkeep");
1068
1276
  if (existingUiModules.length > 0) {
1069
1277
  logger.log(`Error: Currently we support only one UI module per application. Application "${applicationName}" already has a UI module.`);
@@ -1071,148 +1279,1042 @@ async function createUiModule(params, logger) {
1071
1279
  }
1072
1280
  } catch {
1073
1281
  }
1074
- let organizationName;
1075
- let platformName;
1076
- try {
1077
- const corePackageJson = JSON.parse(
1078
- await readFile11(join20(rootDir, "core", "package.json"), "utf-8")
1079
- );
1080
- const scopeMatch = corePackageJson.name.match(/^@([^/]+)\//);
1081
- organizationName = scopeMatch?.[1];
1082
- const rawName = corePackageJson.name.replace(/^@[^/]+\//, "").replace(/-core$/, "");
1083
- platformName = rawName;
1084
- if (!organizationName || !platformName) {
1085
- throw new Error(`Could not parse organization/platform from package name: "${corePackageJson.name}"`);
1086
- }
1087
- } catch (err) {
1088
- logger.log(`Error: Could not read core/package.json \u2014 ${formatError(err)}`);
1089
- return;
1090
- }
1091
- const uiOutputDir = join20(uiDir, `${applicationName}-ui`);
1092
- const uiBaseDir = `${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);
1285
+ const uiBaseDir = `${platformName}-${applicationName}/ui`;
1093
1286
  try {
1094
1287
  await scaffoldUiModule(uiOutputDir, organizationName, platformName, applicationName, applicationDisplayName, uiBaseDir, logger);
1095
1288
  } catch (err) {
1096
1289
  logger.log(`Error: Could not scaffold UI module \u2014 ${formatError(err)}`);
1097
1290
  return;
1098
1291
  }
1099
- const bootstrapServiceDir = join20(applicationDir, "services", `${applicationName}-bootstrap-service`);
1100
- const moduleEntry = buildUiModuleEntry(organizationName, applicationName, applicationDisplayName);
1292
+ const bootstrapServiceDir = join21(applicationDir, "services", `${platformName}-${applicationName}-bootstrap-service`);
1293
+ const moduleEntry = buildUiModuleEntry(organizationName, platformName, applicationName, applicationDisplayName, uiName);
1101
1294
  await addModuleEntry(bootstrapServiceDir, applicationName, moduleEntry, logger);
1102
- const localDir = join20(rootDir, "local");
1103
- const dockerComposePath = join20(localDir, `${applicationName}-docker-compose.yml`);
1295
+ const dockerComposePath = join21(localDir, `${platformName}-${applicationName}-docker-compose.yml`);
1104
1296
  const port = await getNextAvailablePort(localDir);
1105
- await appendUiToDockerCompose(dockerComposePath, applicationName, port, logger);
1106
- logger.log(`Done! UI module "${applicationName}-ui" added to application "${applicationName}".`);
1297
+ await appendUiToDockerCompose(dockerComposePath, platformName, applicationName, port, logger, uiName);
1298
+ logger.log(`Done! UI module "${uiName}" added to application "${applicationName}".`);
1107
1299
  }
1108
1300
 
1109
- // src/commands/registry.ts
1110
- var CommandRegistry = class {
1111
- commands = /* @__PURE__ */ new Map();
1112
- register(command) {
1113
- this.commands.set(command.name, command);
1114
- }
1115
- get(name) {
1116
- return this.commands.get(name);
1117
- }
1118
- getAll() {
1119
- return Array.from(this.commands.values());
1120
- }
1121
- search(query) {
1122
- const all = this.getAll();
1123
- if (!query) return all;
1124
- const fzf = new Fzf(all, {
1125
- selector: (cmd) => `${cmd.name} ${cmd.description}`
1126
- });
1127
- return fzf.find(query).map((result) => result.item);
1128
- }
1129
- };
1130
- var registry = new CommandRegistry();
1131
- registry.register(createApplicationCommand);
1132
- registry.register(initCommand);
1133
- registry.register(configureIdpCommand);
1134
- registry.register(createServiceModuleCommand);
1135
- registry.register(createUiModuleCommand);
1136
-
1137
- // src/app-state.ts
1138
- var APP_STATE = {
1139
- IDLE: "idle",
1140
- PALETTE: "palette",
1141
- EXECUTING: "executing",
1142
- PROMPTING: "prompting"
1143
- };
1144
-
1145
- // src/hooks/use-command-runner.ts
1146
- import { useState as useState2, useCallback, useRef } from "react";
1147
-
1148
- // src/controllers/ui/create-application.ui-controller.ts
1149
- import { readFile as readFile12 } from "fs/promises";
1150
- import { join as join21 } from "path";
1151
- import { cwd as cwd7 } from "process";
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";
1152
1305
 
1153
- // src/services/create-application.service.ts
1154
- async function createApplicationService(params, logger) {
1155
- 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
+ });
1156
1353
  }
1157
-
1158
- // src/controllers/ui/create-application.ui-controller.ts
1159
- async function createApplicationUiController(ctx) {
1160
- if (!await isPlatformInitialized()) {
1161
- ctx.log("Error: Cannot create an application \u2014 no platform initialized in this directory.");
1162
- 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
+ }
1163
1391
  }
1164
- let organizationName;
1165
- 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;
1166
1400
  try {
1167
- const corePackageJson = JSON.parse(
1168
- await readFile12(join21(cwd7(), "core", "package.json"), "utf-8")
1169
- );
1170
- const scopeMatch = corePackageJson.name.match(/^@([^/]+)\//);
1171
- organizationName = scopeMatch?.[1];
1172
- const rawName = corePackageJson.name.replace(/^@[^/]+\//, "").replace(/-core$/, "");
1173
- platformName = rawName;
1174
- if (!organizationName || !platformName) {
1175
- throw new Error(`Could not parse organization/platform from package name: "${corePackageJson.name}"`);
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.`);
1176
1419
  }
1177
- } catch (err) {
1178
- const message = err instanceof Error ? err.message : String(err);
1179
- ctx.log(`Error: Could not read core/package.json \u2014 ${message}`);
1180
- return;
1181
- }
1182
- const applicationName = await ctx.prompt("Application name:");
1183
- if (!/^[a-z0-9-]+$/.test(applicationName)) {
1184
- ctx.log(`Error: Application name "${applicationName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
1185
- return;
1186
1420
  }
1187
- const applicationDisplayName = await ctx.prompt("Application display name:");
1188
- const applicationDescription = await ctx.prompt("Application description:");
1189
- const hasUserInterfaceAnswer = await ctx.prompt("Does this application have a user interface? (yes/no):");
1190
- if (hasUserInterfaceAnswer !== "yes" && hasUserInterfaceAnswer !== "no") {
1191
- ctx.log(`Error: Please answer "yes" or "no".`);
1192
- 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);
1193
1438
  }
1194
- const hasBackendServiceAnswer = await ctx.prompt("Does this application have a backend service? (yes/no):");
1195
- if (hasBackendServiceAnswer !== "yes" && hasBackendServiceAnswer !== "no") {
1196
- ctx.log(`Error: Please answer "yes" or "no".`);
1197
- return;
1439
+ const appEntries = await getAppComposePaths(localDir, platformName, selectedManifest);
1440
+ for (const { composePath } of appEntries) {
1441
+ files.push(composePath);
1198
1442
  }
1199
- await createApplicationService(
1200
- {
1201
- organizationName,
1202
- platformName,
1203
- applicationName,
1204
- applicationDisplayName,
1205
- applicationDescription,
1206
- hasUserInterface: hasUserInterfaceAnswer === "yes",
1207
- hasBackendService: hasBackendServiceAnswer === "yes"
1208
- },
1209
- ctx
1210
- );
1443
+ return files;
1211
1444
  }
1212
-
1213
- // src/services/init.service.ts
1214
- async function initService(params, logger) {
1215
- 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
+ }
1546
+ }
1547
+
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);
1216
2318
  }
1217
2319
 
1218
2320
  // src/controllers/ui/init.ui-controller.ts
@@ -1224,7 +2326,32 @@ async function initUiController(ctx) {
1224
2326
  const organizationName = await ctx.prompt("Organization name:");
1225
2327
  const platformName = await ctx.prompt("Platform name:");
1226
2328
  const platformDisplayName = await ctx.prompt("Platform display name:");
1227
- 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
+ );
1228
2355
  }
1229
2356
 
1230
2357
  // src/services/configure-idp.service.ts
@@ -1239,14 +2366,11 @@ async function configureIdpUiController(ctx) {
1239
2366
  return;
1240
2367
  }
1241
2368
  const providers = getAllProviders();
1242
- const options = providers.map((p, i) => `${i + 1}: ${p.displayName}`).join(", ");
1243
- const selectionInput = await ctx.prompt(`Select IDP type (${options}):`);
1244
- const index = parseInt(selectionInput.trim(), 10) - 1;
1245
- if (isNaN(index) || index < 0 || index >= providers.length) {
1246
- ctx.log(`Error: Invalid selection "${selectionInput}"`);
1247
- return;
1248
- }
1249
- const provider = providers[index];
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);
1250
2374
  const name = await ctx.prompt("Provider name:");
1251
2375
  const issuer = await ctx.prompt("Issuer URL:");
1252
2376
  const clientId = await ctx.prompt("Client ID:");
@@ -1272,19 +2396,38 @@ async function createServiceModuleUiController(ctx) {
1272
2396
  ctx.log("Error: Cannot create a service module \u2014 no platform initialized in this directory.");
1273
2397
  return;
1274
2398
  }
1275
- const applicationName = await ctx.prompt("Application name:");
1276
- if (!/^[a-z0-9-]+$/.test(applicationName)) {
1277
- ctx.log(`Error: Application name "${applicationName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
1278
- return;
2399
+ const layout = await findPlatformLayout();
2400
+ if (!layout) {
2401
+ ctx.log("Error: Cannot create a service module \u2014 no platform initialized in this directory.");
2402
+ return;
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
+ }
1279
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
+ );
1280
2420
  const serviceName = await ctx.prompt("Service name:");
1281
2421
  if (!/^[a-z0-9-]+$/.test(serviceName)) {
1282
2422
  ctx.log(`Error: Service name "${serviceName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
1283
2423
  return;
1284
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;
1285
2428
  const serviceDisplayName = await ctx.prompt("Service display name:");
1286
2429
  await createServiceModuleService(
1287
- { applicationName, serviceName, serviceDisplayName },
2430
+ { applicationName, serviceName, serviceDisplayName, serviceNameSuffix },
1288
2431
  ctx
1289
2432
  );
1290
2433
  }
@@ -1300,40 +2443,355 @@ async function createUiModuleUiController(ctx) {
1300
2443
  ctx.log("Error: Cannot create a UI module \u2014 no platform initialized in this directory.");
1301
2444
  return;
1302
2445
  }
1303
- const applicationName = await ctx.prompt("Application name:");
1304
- if (!/^[a-z0-9-]+$/.test(applicationName)) {
1305
- ctx.log(`Error: Application name "${applicationName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
2446
+ const layout = await findPlatformLayout();
2447
+ if (!layout) {
2448
+ ctx.log("Error: Cannot create a UI module \u2014 no platform initialized in this directory.");
1306
2449
  return;
1307
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;
1308
2470
  const applicationDisplayName = await ctx.prompt("Application display name:");
1309
2471
  await createUiModuleService(
1310
- { applicationName, applicationDisplayName },
2472
+ { applicationName, applicationDisplayName, uiModuleSuffix },
1311
2473
  ctx
1312
2474
  );
1313
2475
  }
1314
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
+
1315
2761
  // src/controllers/ui/registry.ts
1316
2762
  var uiControllers = /* @__PURE__ */ new Map([
1317
2763
  [CREATE_APPLICATION_COMMAND_NAME, createApplicationUiController],
1318
2764
  [INIT_COMMAND_NAME, initUiController],
1319
2765
  [CONFIGURE_IDP_COMMAND_NAME, configureIdpUiController],
1320
2766
  [CREATE_SERVICE_MODULE_COMMAND_NAME, createServiceModuleUiController],
1321
- [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]
1322
2775
  ]);
1323
2776
 
1324
2777
  // src/hooks/use-command-runner.ts
1325
- function useCommandRunner({ appendStaticItem, setState }) {
2778
+ function useCommandRunner({ appendStaticItem, setState, onCommandComplete }) {
1326
2779
  const outputCountRef = useRef(0);
1327
2780
  const abortControllerRef = useRef(null);
1328
2781
  const promptResolveRef = useRef(null);
1329
2782
  const [promptMessage, setPromptMessage] = useState2("");
1330
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());
1331
2788
  const abortExecution = useCallback(() => {
1332
2789
  abortControllerRef.current?.abort();
1333
2790
  abortControllerRef.current = null;
1334
2791
  promptResolveRef.current = null;
2792
+ setPromptMode({ kind: "text" });
1335
2793
  }, []);
1336
- const runCommand = useCallback(
2794
+ const runCommand2 = useCallback(
1337
2795
  (cmd) => {
1338
2796
  const controller = new AbortController();
1339
2797
  abortControllerRef.current = controller;
@@ -1346,15 +2804,73 @@ function useCommandRunner({ appendStaticItem, setState }) {
1346
2804
  appendStaticItem({ kind: "output", id, line: message });
1347
2805
  }
1348
2806
  },
1349
- prompt(message) {
1350
- 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) => {
1351
2866
  if (controller.signal.aborted) {
1352
2867
  reject(new DOMException("Aborted", "AbortError"));
1353
2868
  return;
1354
2869
  }
1355
2870
  setPromptMessage(message);
1356
- setPromptValue("");
1357
- promptResolveRef.current = resolve;
2871
+ setPromptMode({ kind: "confirm" });
2872
+ setConfirmValue(defaultValue ?? true);
2873
+ promptResolveRef.current = (value) => resolve6(value === "true");
1358
2874
  setState(APP_STATE.PROMPTING);
1359
2875
  controller.signal.addEventListener(
1360
2876
  "abort",
@@ -1374,37 +2890,47 @@ function useCommandRunner({ appendStaticItem, setState }) {
1374
2890
  uiController(ctx).then(() => {
1375
2891
  if (!controller.signal.aborted) {
1376
2892
  setState(APP_STATE.IDLE);
2893
+ onCommandComplete?.();
1377
2894
  }
1378
2895
  }).catch((err) => {
1379
2896
  if (controller.signal.aborted) return;
1380
2897
  const id = `output-${outputCountRef.current++}`;
1381
2898
  appendStaticItem({ kind: "output", id, line: `Error: ${formatError(err)}` });
1382
2899
  setState(APP_STATE.IDLE);
2900
+ onCommandComplete?.();
1383
2901
  });
1384
2902
  },
1385
- [appendStaticItem, setState]
2903
+ [appendStaticItem, setState, onCommandComplete]
1386
2904
  );
1387
2905
  const handlePromptSubmit = useCallback(
1388
2906
  (value) => {
1389
- const resolve = promptResolveRef.current;
2907
+ const resolve6 = promptResolveRef.current;
1390
2908
  promptResolveRef.current = null;
2909
+ setPromptMode({ kind: "text" });
1391
2910
  setState(APP_STATE.EXECUTING);
1392
- resolve?.(value);
2911
+ resolve6?.(value);
1393
2912
  },
1394
2913
  [setState]
1395
2914
  );
1396
2915
  return {
1397
- runCommand,
2916
+ runCommand: runCommand2,
1398
2917
  handlePromptSubmit,
1399
2918
  abortExecution,
1400
2919
  promptMessage,
1401
2920
  promptValue,
1402
- setPromptValue
2921
+ setPromptValue,
2922
+ promptMode,
2923
+ selectIndex,
2924
+ setSelectIndex,
2925
+ confirmValue,
2926
+ setConfirmValue,
2927
+ multiselectChecked,
2928
+ setMultiselectChecked
1403
2929
  };
1404
2930
  }
1405
2931
 
1406
2932
  // src/app.tsx
1407
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2933
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
1408
2934
  var require2 = createRequire(import.meta.url);
1409
2935
  var { version } = require2("../package.json");
1410
2936
  function App() {
@@ -1413,12 +2939,35 @@ function App() {
1413
2939
  const [inputValue, setInputValue] = useState3("");
1414
2940
  const [selectedIndex, setSelectedIndex] = useState3(0);
1415
2941
  const [staticItems, setStaticItems] = useState3([{ kind: "banner", id: "banner" }]);
2942
+ const [platformInitialized, setPlatformInitialized] = useState3(false);
2943
+ useEffect2(() => {
2944
+ isPlatformInitialized().then(setPlatformInitialized).catch(() => {
2945
+ });
2946
+ }, []);
1416
2947
  const appendStaticItem = useCallback2((item) => {
1417
2948
  setStaticItems((prev) => [...prev, item]);
1418
2949
  }, []);
1419
- 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 });
1420
2969
  const query = inputValue.startsWith("/") ? inputValue.slice(1) : "";
1421
- const filteredCommands = registry.search(query);
2970
+ const filteredCommands = registry.searchVisible(query, { platformInitialized });
1422
2971
  useInput(
1423
2972
  (input, key) => {
1424
2973
  if (key.ctrl && input === "c") {
@@ -1430,6 +2979,73 @@ function App() {
1430
2979
  setState(APP_STATE.IDLE);
1431
2980
  return;
1432
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
+ }
1433
3049
  if (state === APP_STATE.PALETTE) {
1434
3050
  if (key.escape) {
1435
3051
  setState(APP_STATE.IDLE);
@@ -1448,7 +3064,7 @@ function App() {
1448
3064
  const cmd = filteredCommands[selectedIndex];
1449
3065
  if (cmd) {
1450
3066
  setInputValue("");
1451
- runCommand(cmd);
3067
+ runCommand2(cmd);
1452
3068
  }
1453
3069
  return;
1454
3070
  }
@@ -1484,24 +3100,34 @@ function App() {
1484
3100
  if (!value.startsWith("/")) return;
1485
3101
  const name = value.slice(1).trim();
1486
3102
  const cmd = registry.get(name);
1487
- if (cmd) runCommand(cmd);
3103
+ if (cmd) runCommand2(cmd);
1488
3104
  },
1489
- [runCommand]
3105
+ [runCommand2]
1490
3106
  );
1491
3107
  function renderActiveArea() {
1492
3108
  switch (state) {
1493
3109
  case APP_STATE.EXECUTING:
1494
- return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "row", gap: 1, children: [
1495
- /* @__PURE__ */ jsx6(Spinner, { label: "Running\u2026" }),
1496
- /* @__PURE__ */ jsx6(Text6, { 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)" })
1497
3113
  ] });
1498
3114
  case APP_STATE.PROMPTING:
1499
- return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
1500
- /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: promptMessage }),
1501
- /* @__PURE__ */ jsx6(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 })
1502
3128
  ] });
1503
3129
  default:
1504
- return /* @__PURE__ */ jsx6(
3130
+ return /* @__PURE__ */ jsx10(
1505
3131
  Prompt,
1506
3132
  {
1507
3133
  value: inputValue,
@@ -1514,17 +3140,14 @@ function App() {
1514
3140
  );
1515
3141
  }
1516
3142
  }
1517
- return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
1518
- /* @__PURE__ */ jsx6(ScrollbackHistory, { items: staticItems, version }),
3143
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
3144
+ /* @__PURE__ */ jsx10(ScrollbackHistory, { items: staticItems, version }),
1519
3145
  renderActiveArea(),
1520
- state === APP_STATE.PALETTE && /* @__PURE__ */ jsx6(CommandPalette, { commands: filteredCommands, selectedIndex })
3146
+ state === APP_STATE.PALETTE && /* @__PURE__ */ jsx10(CommandPalette, { commands: filteredCommands, selectedIndex })
1521
3147
  ] });
1522
3148
  }
1523
3149
 
1524
3150
  // src/controllers/cli/create-application.cli-controller.ts
1525
- import { readFile as readFile13 } from "fs/promises";
1526
- import { join as join22 } from "path";
1527
- import { cwd as cwd8 } from "process";
1528
3151
  async function createApplicationCliController(args2) {
1529
3152
  if (!await isPlatformInitialized()) {
1530
3153
  console.error("Error: Cannot create an application \u2014 no platform initialized in this directory.");
@@ -1545,28 +3168,8 @@ async function createApplicationCliController(args2) {
1545
3168
  console.error(`Error: Application name "${applicationName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
1546
3169
  process.exit(1);
1547
3170
  }
1548
- let organizationName;
1549
- let platformName;
1550
- try {
1551
- const corePackageJson = JSON.parse(
1552
- await readFile13(join22(cwd8(), "core", "package.json"), "utf-8")
1553
- );
1554
- const scopeMatch = corePackageJson.name.match(/^@([^/]+)\//);
1555
- organizationName = scopeMatch?.[1];
1556
- const rawName = corePackageJson.name.replace(/^@[^/]+\//, "").replace(/-core$/, "");
1557
- platformName = rawName;
1558
- if (!organizationName || !platformName) {
1559
- throw new Error(`Could not parse organization/platform from package name: "${corePackageJson.name}"`);
1560
- }
1561
- } catch (err) {
1562
- const message = err instanceof Error ? err.message : String(err);
1563
- console.error(`Error: Could not read core/package.json \u2014 ${message}`);
1564
- process.exit(1);
1565
- }
1566
3171
  await createApplicationService(
1567
3172
  {
1568
- organizationName,
1569
- platformName,
1570
3173
  applicationName,
1571
3174
  applicationDisplayName,
1572
3175
  applicationDescription,
@@ -1583,13 +3186,13 @@ async function initCliController(args2) {
1583
3186
  console.error("Error: Cannot initialize a new platform \u2014 a platform is already initialized in this directory.");
1584
3187
  process.exit(1);
1585
3188
  }
1586
- const { organizationName, platformName, platformDisplayName } = args2;
3189
+ const { organizationName, platformName, platformDisplayName, bootstrapServiceSuffix, customizationUiSuffix } = args2;
1587
3190
  if (!organizationName || !platformName || !platformDisplayName) {
1588
3191
  console.error("Error: organizationName, platformName, and platformDisplayName are required.");
1589
3192
  process.exit(1);
1590
3193
  }
1591
3194
  await initService(
1592
- { organizationName, platformName, platformDisplayName },
3195
+ { organizationName, platformName, platformDisplayName, bootstrapServiceSuffix, customizationUiSuffix },
1593
3196
  { log: console.log }
1594
3197
  );
1595
3198
  }
@@ -1623,77 +3226,6 @@ async function configureIdpCliController(args2) {
1623
3226
  await configureIdpService({ providerType, name, issuer, clientId, clientSecret, extras }, logger);
1624
3227
  }
1625
3228
 
1626
- // src/utils/run-npm-script.ts
1627
- import { spawn } from "child_process";
1628
- import { access as access4 } from "fs/promises";
1629
- import { join as join23 } from "path";
1630
- async function runNpmScript(scriptName, logger, signal) {
1631
- const localDir = join23(process.cwd(), "local");
1632
- try {
1633
- await access4(localDir);
1634
- } catch {
1635
- logger.log(`Error: "local/" directory not found. Run "platform init" first.`);
1636
- return;
1637
- }
1638
- return new Promise((resolve) => {
1639
- const child = spawn("npm", ["run", scriptName], {
1640
- cwd: localDir,
1641
- shell: true,
1642
- stdio: ["ignore", "pipe", "pipe"]
1643
- });
1644
- const onAbort = () => {
1645
- child.kill("SIGTERM");
1646
- };
1647
- if (signal) {
1648
- if (signal.aborted) {
1649
- child.kill("SIGTERM");
1650
- resolve();
1651
- return;
1652
- }
1653
- signal.addEventListener("abort", onAbort, { once: true });
1654
- }
1655
- child.stdout.on("data", (data) => {
1656
- for (const line of data.toString().split("\n")) {
1657
- if (line.trim()) logger.log(line);
1658
- }
1659
- });
1660
- child.stderr.on("data", (data) => {
1661
- for (const line of data.toString().split("\n")) {
1662
- if (line.trim()) logger.log(line);
1663
- }
1664
- });
1665
- child.on("close", (code, sig) => {
1666
- signal?.removeEventListener("abort", onAbort);
1667
- if (sig === "SIGTERM" || signal?.aborted) {
1668
- logger.log(`Command cancelled.`);
1669
- } else if (code !== 0) {
1670
- logger.log(`Command "npm run ${scriptName}" exited with code ${code}.`);
1671
- }
1672
- resolve();
1673
- });
1674
- child.on("error", (err) => {
1675
- signal?.removeEventListener("abort", onAbort);
1676
- logger.log(`Failed to run "npm run ${scriptName}": ${err.message}`);
1677
- resolve();
1678
- });
1679
- });
1680
- }
1681
-
1682
- // src/commands/local-scripts/local-script.command.ts
1683
- var INSTALL_COMMAND_NAME = "install";
1684
- var BUILD_COMMAND_NAME = "build";
1685
- var START_COMMAND_NAME = "start";
1686
- var STOP_COMMAND_NAME = "stop";
1687
- var DESTROY_COMMAND_NAME = "destroy";
1688
- async function runLocalScript(scriptName, logger, signal) {
1689
- await runNpmScript(scriptName, logger, signal);
1690
- }
1691
-
1692
- // src/services/local-script.service.ts
1693
- async function localScriptService(scriptName, logger, signal) {
1694
- await runLocalScript(scriptName, logger, signal);
1695
- }
1696
-
1697
3229
  // src/utils/cli-spinner.ts
1698
3230
  var FRAMES2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1699
3231
  var INTERVAL_MS2 = 80;
@@ -1729,10 +3261,11 @@ function createCliSpinner(label) {
1729
3261
 
1730
3262
  // src/controllers/cli/local-script.cli-controller.ts
1731
3263
  function createLocalScriptCliController(scriptName) {
1732
- return async (_args) => {
3264
+ return async (args2) => {
1733
3265
  const spinner = createCliSpinner(`Running ${scriptName}\u2026`);
1734
3266
  spinner.start();
1735
- 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);
1736
3269
  spinner.stop();
1737
3270
  };
1738
3271
  }
@@ -1783,6 +3316,88 @@ async function createUiModuleCliController(args2) {
1783
3316
  );
1784
3317
  }
1785
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
+
1786
3401
  // src/controllers/cli/registry.ts
1787
3402
  var cliControllers = /* @__PURE__ */ new Map([
1788
3403
  [CREATE_APPLICATION_COMMAND_NAME, createApplicationCliController],
@@ -1790,32 +3405,40 @@ var cliControllers = /* @__PURE__ */ new Map([
1790
3405
  [CONFIGURE_IDP_COMMAND_NAME, configureIdpCliController],
1791
3406
  [CREATE_SERVICE_MODULE_COMMAND_NAME, createServiceModuleCliController],
1792
3407
  [CREATE_UI_MODULE_COMMAND_NAME, createUiModuleCliController],
3408
+ [STATUS_COMMAND_NAME, statusCliController],
1793
3409
  [INSTALL_COMMAND_NAME, createLocalScriptCliController(INSTALL_COMMAND_NAME)],
1794
3410
  [BUILD_COMMAND_NAME, createLocalScriptCliController(BUILD_COMMAND_NAME)],
1795
3411
  [START_COMMAND_NAME, createLocalScriptCliController(START_COMMAND_NAME)],
1796
3412
  [STOP_COMMAND_NAME, createLocalScriptCliController(STOP_COMMAND_NAME)],
1797
- [DESTROY_COMMAND_NAME, createLocalScriptCliController(DESTROY_COMMAND_NAME)]
3413
+ [DESTROY_COMMAND_NAME, createLocalScriptCliController(DESTROY_COMMAND_NAME)],
3414
+ [MANAGE_PLATFORM_ADMINS_COMMAND_NAME, managePlatformAdminsCliController]
1798
3415
  ]);
1799
3416
 
1800
3417
  // src/utils/parse-args.ts
1801
3418
  function parseKeyValueArgs(args2) {
1802
3419
  const result = {};
3420
+ const positional = [];
1803
3421
  for (const arg of args2) {
1804
3422
  const eqIndex = arg.indexOf("=");
1805
3423
  if (eqIndex > 0) {
1806
3424
  const key = arg.slice(0, eqIndex);
1807
3425
  const value = arg.slice(eqIndex + 1);
1808
3426
  result[key] = value;
3427
+ } else {
3428
+ positional.push(arg);
1809
3429
  }
1810
3430
  }
3431
+ if (positional.length > 0) {
3432
+ result._positional = JSON.stringify(positional);
3433
+ }
1811
3434
  return result;
1812
3435
  }
1813
3436
 
1814
3437
  // src/index.tsx
1815
- import { jsx as jsx7 } from "react/jsx-runtime";
3438
+ import { jsx as jsx11 } from "react/jsx-runtime";
1816
3439
  var args = process.argv.slice(2);
1817
3440
  if (args.length === 0) {
1818
- render(/* @__PURE__ */ jsx7(App, {}));
3441
+ render(/* @__PURE__ */ jsx11(App, {}));
1819
3442
  } else {
1820
3443
  const commandName = args[0];
1821
3444
  const params = parseKeyValueArgs(args.slice(1));