@bluealba/platform-cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1448 -0
- package/package.json +39 -0
- package/templates/application-data-template/{{applicationName}}/application.json +6 -0
- package/templates/application-data-template/{{applicationName}}/modules.json +1 -0
- package/templates/application-data-template/{{applicationName}}/operations.json +1 -0
- package/templates/application-data-template/{{applicationName}}/roles.json +1 -0
- package/templates/application-monorepo-template/.changeset/config.json +11 -0
- package/templates/application-monorepo-template/.nvmrc +1 -0
- package/templates/application-monorepo-template/.syncpackrc +4 -0
- package/templates/application-monorepo-template/package.json +31 -0
- package/templates/application-monorepo-template/packages-versions.json +1 -0
- package/templates/application-monorepo-template/scripts/preinstall.mjs +57 -0
- package/templates/application-monorepo-template/services/.gitkeep +0 -0
- package/templates/application-monorepo-template/turbo.json +26 -0
- package/templates/application-monorepo-template/ui/.gitkeep +0 -0
- package/templates/bootstrap-service-template/.eslintrc.js +27 -0
- package/templates/bootstrap-service-template/.nvmrc +1 -0
- package/templates/bootstrap-service-template/.prettierrc +4 -0
- package/templates/bootstrap-service-template/Dockerfile +14 -0
- package/templates/bootstrap-service-template/Dockerfile.development +10 -0
- package/templates/bootstrap-service-template/README.md +13 -0
- package/templates/bootstrap-service-template/package.json +38 -0
- package/templates/bootstrap-service-template/src/config.ts +6 -0
- package/templates/bootstrap-service-template/src/data/.gitkeep +0 -0
- package/templates/bootstrap-service-template/src/data/platform/modules-config.json +9 -0
- package/templates/bootstrap-service-template/src/data/platform/modules.json +1 -0
- package/templates/bootstrap-service-template/src/data/shared-libraries.json +14 -0
- package/templates/bootstrap-service-template/src/main.ts +38 -0
- package/templates/bootstrap-service-template/tsconfig.build.json +4 -0
- package/templates/bootstrap-service-template/tsconfig.json +23 -0
- package/templates/customization-ui-module-template/Dockerfile +18 -0
- package/templates/customization-ui-module-template/Dockerfile.development +10 -0
- package/templates/customization-ui-module-template/babel.config.json +36 -0
- package/templates/customization-ui-module-template/caddy/Caddyfile +25 -0
- package/templates/customization-ui-module-template/package.json +59 -0
- package/templates/customization-ui-module-template/src/components/ExpandedNavbarLogo.tsx +62 -0
- package/templates/customization-ui-module-template/src/components/ExtensionPoints/index.tsx +28 -0
- package/templates/customization-ui-module-template/src/components/Logo.tsx +55 -0
- package/templates/customization-ui-module-template/src/components/SplashLogo.tsx +52 -0
- package/templates/customization-ui-module-template/src/declarations.d.ts +41 -0
- package/templates/customization-ui-module-template/src/hooks/useDynamicStyleSheet.ts +18 -0
- package/templates/customization-ui-module-template/src/platform-customization-ui.tsx +16 -0
- package/templates/customization-ui-module-template/src/root.component.tsx +17 -0
- package/templates/customization-ui-module-template/src/styles/base.css +15 -0
- package/templates/customization-ui-module-template/src/styles/index.css +2 -0
- package/templates/customization-ui-module-template/src/styles/platform.css +125 -0
- package/templates/customization-ui-module-template/src/styles/splash.css +7 -0
- package/templates/customization-ui-module-template/tsconfig.json +12 -0
- package/templates/customization-ui-module-template/webpack.config.js +43 -0
- package/templates/nestjs-service-module-template/.env.example +3 -0
- package/templates/nestjs-service-module-template/.eslintrc.js +25 -0
- package/templates/nestjs-service-module-template/.nvmrc +1 -0
- package/templates/nestjs-service-module-template/.prettierrc +4 -0
- package/templates/nestjs-service-module-template/Dockerfile +14 -0
- package/templates/nestjs-service-module-template/Dockerfile.development +12 -0
- package/templates/nestjs-service-module-template/nest-cli.json +8 -0
- package/templates/nestjs-service-module-template/package.json +64 -0
- package/templates/nestjs-service-module-template/src/app.controller.spec.ts +22 -0
- package/templates/nestjs-service-module-template/src/app.controller.ts +12 -0
- package/templates/nestjs-service-module-template/src/app.module.ts +10 -0
- package/templates/nestjs-service-module-template/src/app.service.ts +8 -0
- package/templates/nestjs-service-module-template/src/main.ts +8 -0
- package/templates/nestjs-service-module-template/test/app.e2e-spec.ts +24 -0
- package/templates/nestjs-service-module-template/test/jest-e2e.json +9 -0
- package/templates/nestjs-service-module-template/tsconfig.build.json +4 -0
- package/templates/nestjs-service-module-template/tsconfig.json +21 -0
- package/templates/platform-init-template/core/.changeset/config.json +11 -0
- package/templates/platform-init-template/core/.nvmrc +1 -0
- package/templates/platform-init-template/core/.syncpackrc +4 -0
- package/templates/platform-init-template/core/package.json +69 -0
- package/templates/platform-init-template/core/packages-versions.json +1 -0
- package/templates/platform-init-template/core/scripts/preinstall.mjs +59 -0
- package/templates/platform-init-template/core/services/.gitkeep +0 -0
- package/templates/platform-init-template/core/turbo.json +26 -0
- package/templates/platform-init-template/core/ui/.gitkeep +0 -0
- package/templates/platform-init-template/local/.env.example +18 -0
- package/templates/platform-init-template/local/core-docker-compose.yml +27 -0
- package/templates/platform-init-template/local/docker-compose.yml +3 -0
- package/templates/platform-init-template/local/environment/pae-nestjs-gateway-service.env +34 -0
- package/templates/platform-init-template/local/nginx.conf +69 -0
- package/templates/platform-init-template/local/package.json +18 -0
- package/templates/platform-init-template/local/platform-docker-compose.yml +81 -0
- package/templates/platform-init-template/local/scripts/build.sh +18 -0
- package/templates/platform-init-template/local/scripts/install.sh +18 -0
- package/templates/platform-init-template/local/ssl/cert.pem +21 -0
- package/templates/platform-init-template/local/ssl/key.pem +28 -0
- package/templates/react-ui-module-template/.nvmrc +1 -0
- package/templates/react-ui-module-template/Dockerfile +15 -0
- package/templates/react-ui-module-template/Dockerfile.development +12 -0
- package/templates/react-ui-module-template/Dockerfile_nginx +11 -0
- package/templates/react-ui-module-template/caddy/Caddyfile +21 -0
- package/templates/react-ui-module-template/nginx/default.conf +23 -0
- package/templates/react-ui-module-template/package.json +29 -0
- package/templates/react-ui-module-template/src/Icon.tsx +7 -0
- package/templates/react-ui-module-template/src/components/Icons/BaseIconProps.ts +4 -0
- package/templates/react-ui-module-template/src/components/Icons/HomeTitleIcon.tsx +16 -0
- package/templates/react-ui-module-template/src/components/Icons/InfoContentIcon.tsx +16 -0
- package/templates/react-ui-module-template/src/declarations.d.ts +46 -0
- package/templates/react-ui-module-template/src/hooks/useDebouncedCallback.ts +21 -0
- package/templates/react-ui-module-template/src/main.module.css +10 -0
- package/templates/react-ui-module-template/src/main.tsx +19 -0
- package/templates/react-ui-module-template/src/menu.ts +65 -0
- package/templates/react-ui-module-template/src/root.component.tsx +26 -0
- package/templates/react-ui-module-template/src/services.ts +2 -0
- package/templates/react-ui-module-template/src/views/Home/Home.tsx +30 -0
- package/templates/react-ui-module-template/src/views/Home/index.ts +2 -0
- package/templates/react-ui-module-template/tsconfig.json +30 -0
- package/templates/react-ui-module-template/webpack.config.js +3 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1448 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.tsx
|
|
4
|
+
import { render } from "ink";
|
|
5
|
+
|
|
6
|
+
// src/app.tsx
|
|
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";
|
|
10
|
+
|
|
11
|
+
// src/components/prompt.tsx
|
|
12
|
+
import { Box, Text } from "ink";
|
|
13
|
+
import TextInput from "ink-text-input";
|
|
14
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
+
function Prompt({ value, onChange, onSubmit, disabled = false }) {
|
|
16
|
+
const width = process.stdout.columns || 80;
|
|
17
|
+
const separator = "\u2500".repeat(width);
|
|
18
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
19
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: separator }),
|
|
20
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
21
|
+
/* @__PURE__ */ jsx(Text, { color: "green", children: "> " }),
|
|
22
|
+
disabled ? /* @__PURE__ */ jsx(Text, { children: value }) : /* @__PURE__ */ jsx(
|
|
23
|
+
TextInput,
|
|
24
|
+
{
|
|
25
|
+
value,
|
|
26
|
+
onChange,
|
|
27
|
+
onSubmit,
|
|
28
|
+
placeholder: "Type / to browse commands..."
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
] }),
|
|
32
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: separator })
|
|
33
|
+
] });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/components/command-palette.tsx
|
|
37
|
+
import React from "react";
|
|
38
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
39
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
40
|
+
var CommandPalette = React.memo(function CommandPalette2({ commands, selectedIndex }) {
|
|
41
|
+
if (commands.length === 0) {
|
|
42
|
+
return /* @__PURE__ */ jsx2(Box2, { paddingLeft: 2, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No commands found" }) });
|
|
43
|
+
}
|
|
44
|
+
return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: commands.map((cmd, i) => {
|
|
45
|
+
const isSelected = i === selectedIndex;
|
|
46
|
+
return /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
|
|
47
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "cyan", bold: isSelected, inverse: isSelected, children: [
|
|
48
|
+
"/",
|
|
49
|
+
cmd.name
|
|
50
|
+
] }),
|
|
51
|
+
/* @__PURE__ */ jsx2(Text2, { color: isSelected ? "white" : "gray", children: cmd.description })
|
|
52
|
+
] }, cmd.name);
|
|
53
|
+
}) });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// src/components/scrollback-history.tsx
|
|
57
|
+
import { Static, Text as Text4 } from "ink";
|
|
58
|
+
|
|
59
|
+
// src/components/welcome-banner.tsx
|
|
60
|
+
import React2 from "react";
|
|
61
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
62
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
63
|
+
var ASCII_ART = [
|
|
64
|
+
" \\ | / ",
|
|
65
|
+
" \\ | / ",
|
|
66
|
+
" ----*---- ",
|
|
67
|
+
" / | \\ ",
|
|
68
|
+
" / | \\ "
|
|
69
|
+
];
|
|
70
|
+
var WelcomeBanner = React2.memo(function WelcomeBanner2({ version: version2 }) {
|
|
71
|
+
return /* @__PURE__ */ jsxs3(Box3, { borderStyle: "round", flexDirection: "column", paddingX: 1, paddingY: 1, children: [
|
|
72
|
+
/* @__PURE__ */ jsxs3(Text3, { bold: true, color: "cyan", children: [
|
|
73
|
+
"Blue Alba Platform CLI v",
|
|
74
|
+
version2
|
|
75
|
+
] }),
|
|
76
|
+
/* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
|
|
77
|
+
/* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginRight: 2, children: ASCII_ART.map((line, i) => /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: line }, i)) }),
|
|
78
|
+
/* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", justifyContent: "center", marginRight: 2, children: [
|
|
79
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "Welcome to the" }),
|
|
80
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "Blue Alba Platform!" })
|
|
81
|
+
] }),
|
|
82
|
+
/* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: ASCII_ART.map((_, i) => /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2502" }, i)) }),
|
|
83
|
+
/* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginLeft: 2, children: [
|
|
84
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: "Tips for getting started" }),
|
|
85
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
86
|
+
"Run ",
|
|
87
|
+
/* @__PURE__ */ jsx3(Text3, { color: "green", children: "/init" }),
|
|
88
|
+
" to start building a platform"
|
|
89
|
+
] })
|
|
90
|
+
] })
|
|
91
|
+
] })
|
|
92
|
+
] });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// src/components/scrollback-history.tsx
|
|
96
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
97
|
+
function ScrollbackHistory({ items, version: version2 }) {
|
|
98
|
+
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) });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/components/spinner.tsx
|
|
102
|
+
import { useState, useEffect } from "react";
|
|
103
|
+
import { Text as Text5 } from "ink";
|
|
104
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
105
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
106
|
+
var INTERVAL_MS = 80;
|
|
107
|
+
function Spinner({ label }) {
|
|
108
|
+
const [frame, setFrame] = useState(0);
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const id = setInterval(() => {
|
|
111
|
+
setFrame((prev) => (prev + 1) % FRAMES.length);
|
|
112
|
+
}, INTERVAL_MS);
|
|
113
|
+
return () => clearInterval(id);
|
|
114
|
+
}, []);
|
|
115
|
+
return /* @__PURE__ */ jsxs4(Text5, { children: [
|
|
116
|
+
/* @__PURE__ */ jsxs4(Text5, { color: "cyan", children: [
|
|
117
|
+
FRAMES[frame],
|
|
118
|
+
" "
|
|
119
|
+
] }),
|
|
120
|
+
label && /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: label })
|
|
121
|
+
] });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/commands/registry.ts
|
|
125
|
+
import { Fzf } from "fzf";
|
|
126
|
+
|
|
127
|
+
// src/commands/create-application/create-application.command.ts
|
|
128
|
+
import { join as join9 } from "path";
|
|
129
|
+
import { cwd } from "process";
|
|
130
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
131
|
+
|
|
132
|
+
// src/commands/create-application/scaffold-application-monorepo.ts
|
|
133
|
+
import { fileURLToPath } from "url";
|
|
134
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
135
|
+
|
|
136
|
+
// src/utils/template-engine.ts
|
|
137
|
+
import { readdir, readFile, writeFile, mkdir, copyFile, stat, chmod } from "fs/promises";
|
|
138
|
+
import { join, dirname, extname } from "path";
|
|
139
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
140
|
+
".pem",
|
|
141
|
+
".crt",
|
|
142
|
+
".key",
|
|
143
|
+
".p12",
|
|
144
|
+
".pfx",
|
|
145
|
+
".der",
|
|
146
|
+
".bin",
|
|
147
|
+
".png",
|
|
148
|
+
".jpg",
|
|
149
|
+
".jpeg",
|
|
150
|
+
".gif",
|
|
151
|
+
".ico",
|
|
152
|
+
".woff",
|
|
153
|
+
".woff2",
|
|
154
|
+
".ttf",
|
|
155
|
+
".eot"
|
|
156
|
+
]);
|
|
157
|
+
function applyVariables(text, variables) {
|
|
158
|
+
let result = text;
|
|
159
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
160
|
+
result = result.replaceAll(`{{${key}}}`, value);
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
function isBinary(filePath) {
|
|
165
|
+
return BINARY_EXTENSIONS.has(extname(filePath).toLowerCase());
|
|
166
|
+
}
|
|
167
|
+
async function walk(dir) {
|
|
168
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
169
|
+
const files = [];
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
const fullPath = join(dir, entry.name);
|
|
172
|
+
if (entry.isDirectory()) {
|
|
173
|
+
const nested = await walk(fullPath);
|
|
174
|
+
files.push(...nested);
|
|
175
|
+
} else {
|
|
176
|
+
files.push(fullPath);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return files;
|
|
180
|
+
}
|
|
181
|
+
async function applyTemplate(config, onProgress) {
|
|
182
|
+
const { templateDir: templateDir4, outputDir, variables, exclude = [] } = config;
|
|
183
|
+
const files = await walk(templateDir4);
|
|
184
|
+
for (const srcPath of files) {
|
|
185
|
+
const relativePath = srcPath.slice(templateDir4.length + 1);
|
|
186
|
+
if (relativePath.endsWith(".gitkeep")) continue;
|
|
187
|
+
if (exclude.includes(relativePath)) continue;
|
|
188
|
+
const transformedRelative = applyVariables(relativePath, variables);
|
|
189
|
+
const destPath = join(outputDir, transformedRelative);
|
|
190
|
+
await mkdir(dirname(destPath), { recursive: true });
|
|
191
|
+
if (isBinary(srcPath)) {
|
|
192
|
+
await copyFile(srcPath, destPath);
|
|
193
|
+
} else {
|
|
194
|
+
const content = await readFile(srcPath, "utf8");
|
|
195
|
+
const transformed = applyVariables(content, variables);
|
|
196
|
+
await writeFile(destPath, transformed, "utf8");
|
|
197
|
+
const { mode } = await stat(srcPath);
|
|
198
|
+
await chmod(destPath, mode);
|
|
199
|
+
}
|
|
200
|
+
onProgress?.(` created ${transformedRelative}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/commands/create-application/scaffold-application-monorepo.ts
|
|
205
|
+
var applicationMonorepoTemplateDir = join2(
|
|
206
|
+
dirname2(fileURLToPath(import.meta.url)),
|
|
207
|
+
"..",
|
|
208
|
+
"templates",
|
|
209
|
+
"application-monorepo-template"
|
|
210
|
+
);
|
|
211
|
+
async function scaffoldApplicationMonorepo(outputDir, organizationName, platformName, applicationName, logger) {
|
|
212
|
+
await applyTemplate(
|
|
213
|
+
{
|
|
214
|
+
templateDir: applicationMonorepoTemplateDir,
|
|
215
|
+
outputDir,
|
|
216
|
+
variables: { organizationName, platformName, applicationName }
|
|
217
|
+
},
|
|
218
|
+
(message) => logger.log(message)
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/commands/create-application/scaffold-bootstrap.ts
|
|
223
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
224
|
+
import { join as join3, dirname as dirname3 } from "path";
|
|
225
|
+
var bootstrapTemplateDir = join3(
|
|
226
|
+
dirname3(fileURLToPath2(import.meta.url)),
|
|
227
|
+
"..",
|
|
228
|
+
"templates",
|
|
229
|
+
"bootstrap-service-template"
|
|
230
|
+
);
|
|
231
|
+
async function scaffoldBootstrap(bootstrapServiceDir, organizationName, bootstrapName, bootstrapServiceDir_var, logger) {
|
|
232
|
+
await applyTemplate(
|
|
233
|
+
{
|
|
234
|
+
templateDir: bootstrapTemplateDir,
|
|
235
|
+
outputDir: bootstrapServiceDir,
|
|
236
|
+
variables: { organizationName, bootstrapName, bootstrapServiceDir: bootstrapServiceDir_var },
|
|
237
|
+
exclude: ["src/data/shared-libraries.json", "src/data/platform/modules-config.json"]
|
|
238
|
+
},
|
|
239
|
+
(message) => logger.log(message)
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/commands/create-application/scaffold-application-data.ts
|
|
244
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
245
|
+
import { join as join4, dirname as dirname4 } from "path";
|
|
246
|
+
var applicationTemplateDir = join4(
|
|
247
|
+
dirname4(fileURLToPath3(import.meta.url)),
|
|
248
|
+
"..",
|
|
249
|
+
"templates",
|
|
250
|
+
"application-data-template"
|
|
251
|
+
);
|
|
252
|
+
async function scaffoldApplicationData(bootstrapServiceDir, applicationName, applicationDisplayName, applicationDescription, logger) {
|
|
253
|
+
await applyTemplate(
|
|
254
|
+
{
|
|
255
|
+
templateDir: applicationTemplateDir,
|
|
256
|
+
outputDir: join4(bootstrapServiceDir, "src", "data"),
|
|
257
|
+
variables: { applicationName, applicationDisplayName, applicationDescription }
|
|
258
|
+
},
|
|
259
|
+
(message) => logger.log(message)
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/commands/create-application/scaffold-ui-module.ts
|
|
264
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
265
|
+
import { join as join5, dirname as dirname5 } from "path";
|
|
266
|
+
var reactUiModuleTemplateDir = join5(
|
|
267
|
+
dirname5(fileURLToPath4(import.meta.url)),
|
|
268
|
+
"..",
|
|
269
|
+
"templates",
|
|
270
|
+
"react-ui-module-template"
|
|
271
|
+
);
|
|
272
|
+
async function scaffoldUiModule(uiDir, organizationName, platformName, applicationName, applicationDisplayName, uiBaseDir, logger) {
|
|
273
|
+
logger.log(`Creating UI module "${applicationName}-ui"...`);
|
|
274
|
+
await applyTemplate(
|
|
275
|
+
{
|
|
276
|
+
templateDir: reactUiModuleTemplateDir,
|
|
277
|
+
outputDir: uiDir,
|
|
278
|
+
variables: { organizationName, platformName, applicationName, applicationDisplayName, uiBaseDir }
|
|
279
|
+
},
|
|
280
|
+
(message) => logger.log(message)
|
|
281
|
+
);
|
|
282
|
+
logger.log(`Done! UI output: ${uiDir}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/commands/create-application/scaffold-nestjs-service.ts
|
|
286
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
287
|
+
import { join as join6, dirname as dirname6 } from "path";
|
|
288
|
+
var nestjsServiceModuleTemplateDir = join6(
|
|
289
|
+
dirname6(fileURLToPath5(import.meta.url)),
|
|
290
|
+
"..",
|
|
291
|
+
"templates",
|
|
292
|
+
"nestjs-service-module-template"
|
|
293
|
+
);
|
|
294
|
+
async function scaffoldNestjsService(serviceDir, organizationName, applicationName, applicationDisplayName, serviceBaseDir, logger) {
|
|
295
|
+
const serviceName = `${applicationName}-service`;
|
|
296
|
+
const serviceDisplayName = `${applicationDisplayName} Service`;
|
|
297
|
+
logger.log(`Creating NestJS service "${serviceName}"...`);
|
|
298
|
+
await applyTemplate(
|
|
299
|
+
{
|
|
300
|
+
templateDir: nestjsServiceModuleTemplateDir,
|
|
301
|
+
outputDir: serviceDir,
|
|
302
|
+
variables: { organizationName, serviceName, serviceDisplayName, serviceBaseDir }
|
|
303
|
+
},
|
|
304
|
+
(message) => logger.log(message)
|
|
305
|
+
);
|
|
306
|
+
logger.log(`Done! Service output: ${serviceDir}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/commands/create-application/update-modules-json.ts
|
|
310
|
+
import { join as join7 } from "path";
|
|
311
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
312
|
+
async function addModuleEntry(bootstrapServiceDir, applicationName, entry, logger) {
|
|
313
|
+
const modulesJsonPath = join7(bootstrapServiceDir, "src", "data", applicationName, "modules.json");
|
|
314
|
+
try {
|
|
315
|
+
const modules = JSON.parse(await readFile2(modulesJsonPath, "utf-8"));
|
|
316
|
+
modules.push(entry);
|
|
317
|
+
await writeFile2(modulesJsonPath, JSON.stringify(modules, null, 2) + "\n", "utf-8");
|
|
318
|
+
logger.log(`Updated modules.json: ${modulesJsonPath}`);
|
|
319
|
+
} catch (err) {
|
|
320
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
321
|
+
logger.log(`Warning: Could not update modules.json \u2014 ${message}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/commands/create-application/module-entry-builders.ts
|
|
326
|
+
function buildServiceModuleEntry(organizationName, applicationName, applicationDisplayName) {
|
|
327
|
+
return {
|
|
328
|
+
name: `@${organizationName}/${applicationName}-service`,
|
|
329
|
+
displayName: `${applicationDisplayName} Service`,
|
|
330
|
+
type: "service",
|
|
331
|
+
baseUrl: `/${applicationName}-service`,
|
|
332
|
+
service: {
|
|
333
|
+
host: `${applicationName}-service`,
|
|
334
|
+
port: 80
|
|
335
|
+
},
|
|
336
|
+
dependsOn: []
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function buildUiModuleEntry(organizationName, applicationName, applicationDisplayName) {
|
|
340
|
+
return {
|
|
341
|
+
name: `@${organizationName}/${applicationName}-ui`,
|
|
342
|
+
displayName: applicationDisplayName,
|
|
343
|
+
type: "app",
|
|
344
|
+
baseUrl: `/${applicationName}-ui`,
|
|
345
|
+
service: {
|
|
346
|
+
host: `${applicationName}-ui`,
|
|
347
|
+
port: 80
|
|
348
|
+
},
|
|
349
|
+
ui: {
|
|
350
|
+
route: `/${applicationName}`,
|
|
351
|
+
mountAtSelector: "#pae-shell-ui-content",
|
|
352
|
+
bundleFile: `${organizationName}-${applicationName}-ui.js`,
|
|
353
|
+
customProps: {}
|
|
354
|
+
},
|
|
355
|
+
dependsOn: [`@bluealba/pae-shell-ui`]
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/commands/create-application/create-docker-compose.ts
|
|
360
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
361
|
+
function buildBootstrapBlock(applicationName) {
|
|
362
|
+
return ` ${applicationName}-bootstrap-service:
|
|
363
|
+
build:
|
|
364
|
+
context: ../${applicationName}/services/${applicationName}-bootstrap-service
|
|
365
|
+
dockerfile: Dockerfile.development
|
|
366
|
+
environment:
|
|
367
|
+
- NODE_TLS_REJECT_UNAUTHORIZED=0
|
|
368
|
+
- SERVICE_ACCESS_NAME=${applicationName}-bootstrap
|
|
369
|
+
- WAIT_TIME=5000
|
|
370
|
+
- SYNC_STRATEGY=\${PAE_BOOTSTRAP_SYNC_STRATEGY}
|
|
371
|
+
- GATEWAY_SERVICE_URL=\${PAE_GATEWAY_URL}
|
|
372
|
+
- SERVICE_ACCESS_SECRET=\${PAE_GATEWAY_SERVICE_ACCESS_SECRET}
|
|
373
|
+
volumes:
|
|
374
|
+
- \${PWD}/../:/app/out
|
|
375
|
+
depends_on:
|
|
376
|
+
pae-nestjs-gateway-service:
|
|
377
|
+
condition: service_healthy
|
|
378
|
+
`;
|
|
379
|
+
}
|
|
380
|
+
function buildUiBlock(applicationName, uiPort) {
|
|
381
|
+
return ` ${applicationName}-ui:
|
|
382
|
+
build:
|
|
383
|
+
context: ../${applicationName}/ui/${applicationName}-ui
|
|
384
|
+
dockerfile: Dockerfile.development
|
|
385
|
+
ports:
|
|
386
|
+
- ${uiPort}:80
|
|
387
|
+
volumes:
|
|
388
|
+
- \${PWD}/../:/app/out
|
|
389
|
+
`;
|
|
390
|
+
}
|
|
391
|
+
function buildBackendBlock(applicationName, servicePort) {
|
|
392
|
+
return ` ${applicationName}-service:
|
|
393
|
+
build:
|
|
394
|
+
context: ../${applicationName}/services/${applicationName}-service
|
|
395
|
+
dockerfile: Dockerfile.development
|
|
396
|
+
args:
|
|
397
|
+
- BA_NPM_AUTH_TOKEN=$BA_NPM_AUTH_TOKEN
|
|
398
|
+
ports:
|
|
399
|
+
- ${servicePort}:80
|
|
400
|
+
environment:
|
|
401
|
+
- GATEWAY_URL=\${PAE_GATEWAY_URL}
|
|
402
|
+
volumes:
|
|
403
|
+
- \${PWD}/../:/app/out
|
|
404
|
+
`;
|
|
405
|
+
}
|
|
406
|
+
async function createDockerCompose(dockerComposePath, applicationName, hasUserInterface, hasBackendService, uiPort, servicePort, logger) {
|
|
407
|
+
const blocks = ["services:\n", buildBootstrapBlock(applicationName)];
|
|
408
|
+
if (hasUserInterface) {
|
|
409
|
+
blocks.push(buildUiBlock(applicationName, uiPort));
|
|
410
|
+
}
|
|
411
|
+
if (hasBackendService) {
|
|
412
|
+
blocks.push(buildBackendBlock(applicationName, servicePort));
|
|
413
|
+
}
|
|
414
|
+
const content = blocks.join("\n");
|
|
415
|
+
try {
|
|
416
|
+
await writeFile3(dockerComposePath, content, "utf-8");
|
|
417
|
+
logger.log(`Created docker-compose: ${dockerComposePath}`);
|
|
418
|
+
} catch (err) {
|
|
419
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
420
|
+
logger.log(`Warning: Could not create docker-compose \u2014 ${message}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/commands/create-application/update-root-docker-compose.ts
|
|
425
|
+
import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
|
|
426
|
+
async function updateRootDockerCompose(rootDockerComposePath, applicationName, logger) {
|
|
427
|
+
try {
|
|
428
|
+
const existing = await readFile3(rootDockerComposePath, "utf-8");
|
|
429
|
+
const includeLine = ` - path: ./${applicationName}-docker-compose.yml
|
|
430
|
+
`;
|
|
431
|
+
await writeFile4(rootDockerComposePath, existing + includeLine, "utf-8");
|
|
432
|
+
logger.log(`Updated root docker-compose: ${rootDockerComposePath}`);
|
|
433
|
+
} catch (err) {
|
|
434
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
435
|
+
logger.log(`Warning: Could not update root docker-compose \u2014 ${message}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/commands/create-application/port-allocator.ts
|
|
440
|
+
import { join as join8 } from "path";
|
|
441
|
+
import { readdir as readdir2, readFile as readFile4 } from "fs/promises";
|
|
442
|
+
async function getNextAvailablePort(localDir) {
|
|
443
|
+
const allPorts = [];
|
|
444
|
+
try {
|
|
445
|
+
const files = await readdir2(localDir);
|
|
446
|
+
const dockerComposeFiles = files.filter((f) => f.endsWith("-docker-compose.yml"));
|
|
447
|
+
for (const file of dockerComposeFiles) {
|
|
448
|
+
try {
|
|
449
|
+
const content = await readFile4(join8(localDir, file), "utf-8");
|
|
450
|
+
const regex = /^\s*-\s*"?(\d+):\d+"?\s*$/gm;
|
|
451
|
+
let match;
|
|
452
|
+
while ((match = regex.exec(content)) !== null) {
|
|
453
|
+
allPorts.push(parseInt(match[1], 10));
|
|
454
|
+
}
|
|
455
|
+
} catch {
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
} catch {
|
|
459
|
+
}
|
|
460
|
+
return allPorts.length > 0 ? Math.max(...allPorts) + 1 : 9e3;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/utils/error.ts
|
|
464
|
+
function formatError(err) {
|
|
465
|
+
return err instanceof Error ? err.message : String(err);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/commands/create-application/create-application.command.ts
|
|
469
|
+
var CREATE_APPLICATION_COMMAND_NAME = "create-application";
|
|
470
|
+
var createApplicationCommand = {
|
|
471
|
+
name: CREATE_APPLICATION_COMMAND_NAME,
|
|
472
|
+
description: "Create an application in a platform"
|
|
473
|
+
};
|
|
474
|
+
async function createApplication(params, logger) {
|
|
475
|
+
const {
|
|
476
|
+
organizationName,
|
|
477
|
+
platformName,
|
|
478
|
+
applicationName,
|
|
479
|
+
applicationDisplayName,
|
|
480
|
+
applicationDescription,
|
|
481
|
+
hasUserInterface,
|
|
482
|
+
hasBackendService
|
|
483
|
+
} = params;
|
|
484
|
+
const rootDir = cwd();
|
|
485
|
+
const applicationDir = join9(rootDir, applicationName);
|
|
486
|
+
const bootstrapServiceDir = join9(applicationDir, "services", `${applicationName}-bootstrap-service`);
|
|
487
|
+
const localDir = join9(rootDir, "local");
|
|
488
|
+
logger.log(`Creating application monorepo "${applicationName}"...`);
|
|
489
|
+
try {
|
|
490
|
+
await scaffoldApplicationMonorepo(applicationDir, organizationName, platformName, applicationName, logger);
|
|
491
|
+
} catch (err) {
|
|
492
|
+
logger.log(`Error: Could not scaffold application monorepo \u2014 ${formatError(err)}`);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
await mkdir2(join9(applicationDir, "services"), { recursive: true });
|
|
496
|
+
await mkdir2(join9(applicationDir, "ui"), { recursive: true });
|
|
497
|
+
logger.log(`Creating bootstrap service "${applicationName}-bootstrap-service"...`);
|
|
498
|
+
const bootstrapServiceDir_var = `${applicationName}/services`;
|
|
499
|
+
try {
|
|
500
|
+
await scaffoldBootstrap(bootstrapServiceDir, organizationName, applicationName, bootstrapServiceDir_var, logger);
|
|
501
|
+
} catch (err) {
|
|
502
|
+
logger.log(`Error: Could not scaffold bootstrap service \u2014 ${formatError(err)}`);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
logger.log(`Adding application data "${applicationName}"...`);
|
|
506
|
+
try {
|
|
507
|
+
await scaffoldApplicationData(bootstrapServiceDir, applicationName, applicationDisplayName, applicationDescription, logger);
|
|
508
|
+
} catch (err) {
|
|
509
|
+
logger.log(`Error: Could not scaffold application data \u2014 ${formatError(err)}`);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (hasUserInterface) {
|
|
513
|
+
await addModuleEntry(
|
|
514
|
+
bootstrapServiceDir,
|
|
515
|
+
applicationName,
|
|
516
|
+
buildUiModuleEntry(organizationName, applicationName, applicationDisplayName),
|
|
517
|
+
logger
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
if (hasBackendService) {
|
|
521
|
+
await addModuleEntry(
|
|
522
|
+
bootstrapServiceDir,
|
|
523
|
+
applicationName,
|
|
524
|
+
buildServiceModuleEntry(organizationName, applicationName, applicationDisplayName),
|
|
525
|
+
logger
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
if (hasUserInterface) {
|
|
529
|
+
const uiDir = join9(applicationDir, "ui", `${applicationName}-ui`);
|
|
530
|
+
const uiBaseDir = `${applicationName}/ui`;
|
|
531
|
+
try {
|
|
532
|
+
await scaffoldUiModule(uiDir, organizationName, platformName, applicationName, applicationDisplayName, uiBaseDir, logger);
|
|
533
|
+
} catch (err) {
|
|
534
|
+
logger.log(`Error: Could not scaffold UI module \u2014 ${formatError(err)}`);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (hasBackendService) {
|
|
539
|
+
const serviceDir = join9(applicationDir, "services", `${applicationName}-service`);
|
|
540
|
+
const serviceBaseDir = `${applicationName}/services`;
|
|
541
|
+
try {
|
|
542
|
+
await scaffoldNestjsService(serviceDir, organizationName, applicationName, applicationDisplayName, serviceBaseDir, logger);
|
|
543
|
+
} catch (err) {
|
|
544
|
+
logger.log(`Error: Could not scaffold NestJS service \u2014 ${formatError(err)}`);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const dockerComposePath = join9(localDir, `${applicationName}-docker-compose.yml`);
|
|
549
|
+
const basePort = await getNextAvailablePort(localDir);
|
|
550
|
+
const uiPort = hasUserInterface ? basePort : 0;
|
|
551
|
+
const servicePort = hasBackendService ? hasUserInterface ? basePort + 1 : basePort : 0;
|
|
552
|
+
await createDockerCompose(
|
|
553
|
+
dockerComposePath,
|
|
554
|
+
applicationName,
|
|
555
|
+
hasUserInterface,
|
|
556
|
+
hasBackendService,
|
|
557
|
+
uiPort,
|
|
558
|
+
servicePort,
|
|
559
|
+
logger
|
|
560
|
+
);
|
|
561
|
+
const rootDockerComposePath = join9(localDir, "docker-compose.yml");
|
|
562
|
+
await updateRootDockerCompose(rootDockerComposePath, applicationName, logger);
|
|
563
|
+
logger.log(`Done! Application "${applicationName}" created at ${applicationDir}`);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/commands/init/init.command.ts
|
|
567
|
+
import { join as join15 } from "path";
|
|
568
|
+
import { cwd as cwd2 } from "process";
|
|
569
|
+
|
|
570
|
+
// src/utils/string.ts
|
|
571
|
+
function camelize(name) {
|
|
572
|
+
return name.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/commands/init/scaffold-platform.ts
|
|
576
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
577
|
+
import { join as join10, dirname as dirname7 } from "path";
|
|
578
|
+
var templateDir = join10(
|
|
579
|
+
dirname7(fileURLToPath6(import.meta.url)),
|
|
580
|
+
"..",
|
|
581
|
+
"templates",
|
|
582
|
+
"platform-init-template"
|
|
583
|
+
);
|
|
584
|
+
async function scaffoldPlatform(outputDir, variables, logger) {
|
|
585
|
+
await applyTemplate({ templateDir, outputDir, variables }, (message) => logger.log(message));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/commands/init/scaffold-platform-bootstrap.ts
|
|
589
|
+
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
590
|
+
import { join as join11, dirname as dirname8 } from "path";
|
|
591
|
+
var templateDir2 = join11(
|
|
592
|
+
dirname8(fileURLToPath7(import.meta.url)),
|
|
593
|
+
"..",
|
|
594
|
+
"templates",
|
|
595
|
+
"bootstrap-service-template"
|
|
596
|
+
);
|
|
597
|
+
async function scaffoldPlatformBootstrap(outputDir, variables, logger) {
|
|
598
|
+
await applyTemplate({ templateDir: templateDir2, outputDir, variables }, (message) => logger.log(message));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/commands/init/scaffold-customization-ui.ts
|
|
602
|
+
import { fileURLToPath as fileURLToPath8 } from "url";
|
|
603
|
+
import { join as join12, dirname as dirname9 } from "path";
|
|
604
|
+
var templateDir3 = join12(
|
|
605
|
+
dirname9(fileURLToPath8(import.meta.url)),
|
|
606
|
+
"..",
|
|
607
|
+
"templates",
|
|
608
|
+
"customization-ui-module-template"
|
|
609
|
+
);
|
|
610
|
+
async function scaffoldCustomizationUi(outputDir, variables, logger) {
|
|
611
|
+
await applyTemplate({ templateDir: templateDir3, outputDir, variables }, (message) => logger.log(message));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// src/commands/init/register-customization-module.ts
|
|
615
|
+
import { join as join13 } from "path";
|
|
616
|
+
import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
617
|
+
async function registerCustomizationModule(bootstrapServiceDir, organizationName, logger) {
|
|
618
|
+
const modulesJsonPath = join13(bootstrapServiceDir, "src", "data", "platform", "modules.json");
|
|
619
|
+
const entry = {
|
|
620
|
+
name: `@${organizationName}/platform-customization-ui`,
|
|
621
|
+
displayName: "Platform Customization UI",
|
|
622
|
+
type: "app",
|
|
623
|
+
baseUrl: "/platform-customization-ui",
|
|
624
|
+
service: { host: "platform-customization-ui", port: 80 },
|
|
625
|
+
ui: {
|
|
626
|
+
route: "/",
|
|
627
|
+
bundleFile: "platform-customization-ui.js",
|
|
628
|
+
isPlatformCustomization: true,
|
|
629
|
+
customProps: {}
|
|
630
|
+
},
|
|
631
|
+
dependsOn: []
|
|
632
|
+
};
|
|
633
|
+
const existing = JSON.parse(await readFile5(modulesJsonPath, "utf-8"));
|
|
634
|
+
existing.push(entry);
|
|
635
|
+
await writeFile5(modulesJsonPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
636
|
+
logger.log(`Registered customization module in ${modulesJsonPath}`);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/commands/init/generate-local-env.ts
|
|
640
|
+
import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
|
|
641
|
+
import { join as join14 } from "path";
|
|
642
|
+
|
|
643
|
+
// src/utils/random.ts
|
|
644
|
+
import { randomBytes } from "crypto";
|
|
645
|
+
function generateRandomSecret() {
|
|
646
|
+
return randomBytes(32).toString("hex");
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/commands/init/generate-local-env.ts
|
|
650
|
+
async function generateLocalEnv(outputDir, logger) {
|
|
651
|
+
const examplePath = join14(outputDir, "local", ".env.example");
|
|
652
|
+
const envPath = join14(outputDir, "local", ".env");
|
|
653
|
+
logger.log("Generating local/.env with random secrets...");
|
|
654
|
+
const content = await readFile6(examplePath, "utf-8");
|
|
655
|
+
const result = content.replace(/^(PAE_AUTH_JWT_SECRET|PAE_GATEWAY_SERVICE_ACCESS_SECRET|PAE_DB_PASSWORD)=$/gm, (_, key) => {
|
|
656
|
+
return `${key}=${generateRandomSecret()}`;
|
|
657
|
+
});
|
|
658
|
+
await writeFile6(envPath, result, "utf-8");
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/commands/init/init.command.ts
|
|
662
|
+
var INIT_COMMAND_NAME = "init";
|
|
663
|
+
var initCommand = {
|
|
664
|
+
name: INIT_COMMAND_NAME,
|
|
665
|
+
description: "Initialize a new platform"
|
|
666
|
+
};
|
|
667
|
+
async function init(params, logger) {
|
|
668
|
+
const { organizationName, platformName, platformDisplayName } = params;
|
|
669
|
+
const platformTitle = camelize(platformName);
|
|
670
|
+
const outputDir = cwd2();
|
|
671
|
+
logger.log(`Initializing ${platformTitle} platform for @${organizationName}...`);
|
|
672
|
+
const variables = {
|
|
673
|
+
organizationName,
|
|
674
|
+
platformName,
|
|
675
|
+
platformTitle,
|
|
676
|
+
platformDisplayName,
|
|
677
|
+
bootstrapName: "platform",
|
|
678
|
+
bootstrapServiceDir: "core/services"
|
|
679
|
+
};
|
|
680
|
+
try {
|
|
681
|
+
await scaffoldPlatform(outputDir, variables, logger);
|
|
682
|
+
} catch (err) {
|
|
683
|
+
logger.log(`Error: Could not initialize platform \u2014 ${formatError(err)}`);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
await generateLocalEnv(outputDir, logger);
|
|
688
|
+
} catch (err) {
|
|
689
|
+
logger.log(`Error: Could not generate local/.env \u2014 ${formatError(err)}`);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
try {
|
|
693
|
+
await scaffoldPlatformBootstrap(
|
|
694
|
+
join15(outputDir, "core", "services", "platform-bootstrap-service"),
|
|
695
|
+
variables,
|
|
696
|
+
logger
|
|
697
|
+
);
|
|
698
|
+
} catch (err) {
|
|
699
|
+
logger.log(`Error: Could not generate bootstrap service \u2014 ${formatError(err)}`);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
try {
|
|
703
|
+
await scaffoldCustomizationUi(
|
|
704
|
+
join15(outputDir, "core", "ui", "platform-customization-ui"),
|
|
705
|
+
variables,
|
|
706
|
+
logger
|
|
707
|
+
);
|
|
708
|
+
} catch (err) {
|
|
709
|
+
logger.log(`Error: Could not generate customization UI module \u2014 ${formatError(err)}`);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
await registerCustomizationModule(
|
|
714
|
+
join15(outputDir, "core", "services", "platform-bootstrap-service"),
|
|
715
|
+
organizationName,
|
|
716
|
+
logger
|
|
717
|
+
);
|
|
718
|
+
} catch (err) {
|
|
719
|
+
logger.log(`Error: Could not register customization module \u2014 ${formatError(err)}`);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
logger.log(`Done! Output: ${outputDir}`);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// src/commands/configure-idp/configure-idp.command.ts
|
|
726
|
+
import { join as join16 } from "path";
|
|
727
|
+
import { cwd as cwd3 } from "process";
|
|
728
|
+
import { fetch as undiciFetch, Agent } from "undici";
|
|
729
|
+
|
|
730
|
+
// src/utils/env-reader.ts
|
|
731
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
732
|
+
async function readEnvFile(filePath) {
|
|
733
|
+
let content;
|
|
734
|
+
try {
|
|
735
|
+
content = await readFile7(filePath, "utf-8");
|
|
736
|
+
} catch (error) {
|
|
737
|
+
if (error.code === "ENOENT") {
|
|
738
|
+
throw new Error(`.env file not found at ${filePath}`);
|
|
739
|
+
}
|
|
740
|
+
throw error;
|
|
741
|
+
}
|
|
742
|
+
const result = /* @__PURE__ */ new Map();
|
|
743
|
+
for (const line of content.split("\n")) {
|
|
744
|
+
const trimmed = line.trim();
|
|
745
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
746
|
+
const eqIndex = trimmed.indexOf("=");
|
|
747
|
+
if (eqIndex === -1) continue;
|
|
748
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
749
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
750
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
751
|
+
value = value.slice(1, -1);
|
|
752
|
+
}
|
|
753
|
+
result.set(key, value);
|
|
754
|
+
}
|
|
755
|
+
return result;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// src/commands/configure-idp/idp-provider-registry.ts
|
|
759
|
+
var oktaProvider = {
|
|
760
|
+
type: "okta",
|
|
761
|
+
displayName: "Okta",
|
|
762
|
+
fields: [],
|
|
763
|
+
buildPayload(params) {
|
|
764
|
+
return {
|
|
765
|
+
type: "okta",
|
|
766
|
+
name: params.name,
|
|
767
|
+
issuer: params.issuer,
|
|
768
|
+
clientId: params.clientId,
|
|
769
|
+
clientSecret: params.clientSecret
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
var entraIdProvider = {
|
|
774
|
+
type: "entra-id",
|
|
775
|
+
displayName: "Entra ID",
|
|
776
|
+
fields: [{ key: "tenantId", prompt: "Tenant ID:" }],
|
|
777
|
+
buildPayload(params) {
|
|
778
|
+
return {
|
|
779
|
+
type: "entra-id",
|
|
780
|
+
name: params.name,
|
|
781
|
+
issuer: params.issuer,
|
|
782
|
+
clientId: params.clientId,
|
|
783
|
+
clientSecret: params.clientSecret,
|
|
784
|
+
extras: {
|
|
785
|
+
tenantId: params.extras?.tenantId ?? ""
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
var idpProviderRegistry = /* @__PURE__ */ new Map([
|
|
791
|
+
[oktaProvider.type, oktaProvider],
|
|
792
|
+
[entraIdProvider.type, entraIdProvider]
|
|
793
|
+
]);
|
|
794
|
+
function getAllProviders() {
|
|
795
|
+
return Array.from(idpProviderRegistry.values());
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// src/commands/configure-idp/configure-idp.command.ts
|
|
799
|
+
var CONFIGURE_IDP_COMMAND_NAME = "configure-idp";
|
|
800
|
+
var configureIdpCommand = {
|
|
801
|
+
name: CONFIGURE_IDP_COMMAND_NAME,
|
|
802
|
+
description: "Configure an Identity Provider (IDP) in the gateway"
|
|
803
|
+
};
|
|
804
|
+
async function configureIdp(params, logger) {
|
|
805
|
+
const envPath = join16(cwd3(), "local", ".env");
|
|
806
|
+
let env;
|
|
807
|
+
try {
|
|
808
|
+
env = await readEnvFile(envPath);
|
|
809
|
+
} catch (error) {
|
|
810
|
+
logger.log(`Error: ${error.message}`);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const gatewayUrl = env.get("PAE_GATEWAY_HOST_URL");
|
|
814
|
+
const accessSecret = env.get("PAE_GATEWAY_SERVICE_ACCESS_SECRET");
|
|
815
|
+
if (!gatewayUrl) {
|
|
816
|
+
logger.log("Error: PAE_GATEWAY_HOST_URL is not set in local/.env");
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
if (!accessSecret) {
|
|
820
|
+
logger.log("Error: PAE_GATEWAY_SERVICE_ACCESS_SECRET is not set in local/.env");
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
const provider = idpProviderRegistry.get(params.providerType);
|
|
824
|
+
if (!provider) {
|
|
825
|
+
logger.log(`Error: Unknown IDP type "${params.providerType}"`);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
const payload = provider.buildPayload(params);
|
|
829
|
+
const agent = new Agent({ connect: { rejectUnauthorized: false } });
|
|
830
|
+
let response;
|
|
831
|
+
try {
|
|
832
|
+
response = await undiciFetch(`${gatewayUrl}/_/providers`, {
|
|
833
|
+
method: "POST",
|
|
834
|
+
headers: {
|
|
835
|
+
"Content-Type": "application/json",
|
|
836
|
+
"Accept": "application/json",
|
|
837
|
+
Authorization: `Bearer ${accessSecret}`
|
|
838
|
+
},
|
|
839
|
+
body: JSON.stringify(payload),
|
|
840
|
+
dispatcher: agent
|
|
841
|
+
});
|
|
842
|
+
} catch (error) {
|
|
843
|
+
const err = error;
|
|
844
|
+
const cause = err.cause instanceof Error ? ` (cause: ${err.cause.message})` : err.cause ? ` (cause: ${String(err.cause)})` : "";
|
|
845
|
+
logger.log(`Error: Failed to reach gateway \u2014 ${err.message}${cause}`);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
if (!response.ok) {
|
|
849
|
+
const body = await response.text().catch(() => "");
|
|
850
|
+
logger.log(`Error: Server responded with ${response.status}${body ? ` \u2014 ${body}` : ""}`);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
logger.log(`IDP provider "${params.name}" configured successfully.`);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// src/commands/registry.ts
|
|
857
|
+
var CommandRegistry = class {
|
|
858
|
+
commands = /* @__PURE__ */ new Map();
|
|
859
|
+
register(command) {
|
|
860
|
+
this.commands.set(command.name, command);
|
|
861
|
+
}
|
|
862
|
+
get(name) {
|
|
863
|
+
return this.commands.get(name);
|
|
864
|
+
}
|
|
865
|
+
getAll() {
|
|
866
|
+
return Array.from(this.commands.values());
|
|
867
|
+
}
|
|
868
|
+
search(query) {
|
|
869
|
+
const all = this.getAll();
|
|
870
|
+
if (!query) return all;
|
|
871
|
+
const fzf = new Fzf(all, {
|
|
872
|
+
selector: (cmd) => `${cmd.name} ${cmd.description}`
|
|
873
|
+
});
|
|
874
|
+
return fzf.find(query).map((result) => result.item);
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
var registry = new CommandRegistry();
|
|
878
|
+
registry.register(createApplicationCommand);
|
|
879
|
+
registry.register(initCommand);
|
|
880
|
+
registry.register(configureIdpCommand);
|
|
881
|
+
|
|
882
|
+
// src/app-state.ts
|
|
883
|
+
var APP_STATE = {
|
|
884
|
+
IDLE: "idle",
|
|
885
|
+
PALETTE: "palette",
|
|
886
|
+
EXECUTING: "executing",
|
|
887
|
+
PROMPTING: "prompting"
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
// src/hooks/use-command-runner.ts
|
|
891
|
+
import { useState as useState2, useCallback, useRef } from "react";
|
|
892
|
+
|
|
893
|
+
// src/controllers/ui/create-application.ui-controller.ts
|
|
894
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
895
|
+
import { join as join17 } from "path";
|
|
896
|
+
import { cwd as cwd4 } from "process";
|
|
897
|
+
|
|
898
|
+
// src/services/create-application.service.ts
|
|
899
|
+
async function createApplicationService(params, logger) {
|
|
900
|
+
await createApplication(params, logger);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// src/controllers/ui/create-application.ui-controller.ts
|
|
904
|
+
async function createApplicationUiController(ctx) {
|
|
905
|
+
let organizationName;
|
|
906
|
+
let platformName;
|
|
907
|
+
try {
|
|
908
|
+
const corePackageJson = JSON.parse(
|
|
909
|
+
await readFile8(join17(cwd4(), "core", "package.json"), "utf-8")
|
|
910
|
+
);
|
|
911
|
+
const scopeMatch = corePackageJson.name.match(/^@([^/]+)\//);
|
|
912
|
+
organizationName = scopeMatch?.[1];
|
|
913
|
+
const rawName = corePackageJson.name.replace(/^@[^/]+\//, "").replace(/-core$/, "");
|
|
914
|
+
platformName = rawName;
|
|
915
|
+
if (!organizationName || !platformName) {
|
|
916
|
+
throw new Error(`Could not parse organization/platform from package name: "${corePackageJson.name}"`);
|
|
917
|
+
}
|
|
918
|
+
} catch (err) {
|
|
919
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
920
|
+
ctx.log(`Error: Could not read core/package.json \u2014 ${message}`);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
const applicationName = await ctx.prompt("Application name:");
|
|
924
|
+
if (!/^[a-z0-9-]+$/.test(applicationName)) {
|
|
925
|
+
ctx.log(`Error: Application name "${applicationName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const applicationDisplayName = await ctx.prompt("Application display name:");
|
|
929
|
+
const applicationDescription = await ctx.prompt("Application description:");
|
|
930
|
+
const hasUserInterfaceAnswer = await ctx.prompt("Does this application have a user interface? (yes/no):");
|
|
931
|
+
if (hasUserInterfaceAnswer !== "yes" && hasUserInterfaceAnswer !== "no") {
|
|
932
|
+
ctx.log(`Error: Please answer "yes" or "no".`);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
const hasBackendServiceAnswer = await ctx.prompt("Does this application have a backend service? (yes/no):");
|
|
936
|
+
if (hasBackendServiceAnswer !== "yes" && hasBackendServiceAnswer !== "no") {
|
|
937
|
+
ctx.log(`Error: Please answer "yes" or "no".`);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
await createApplicationService(
|
|
941
|
+
{
|
|
942
|
+
organizationName,
|
|
943
|
+
platformName,
|
|
944
|
+
applicationName,
|
|
945
|
+
applicationDisplayName,
|
|
946
|
+
applicationDescription,
|
|
947
|
+
hasUserInterface: hasUserInterfaceAnswer === "yes",
|
|
948
|
+
hasBackendService: hasBackendServiceAnswer === "yes"
|
|
949
|
+
},
|
|
950
|
+
ctx
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// src/services/init.service.ts
|
|
955
|
+
async function initService(params, logger) {
|
|
956
|
+
await init(params, logger);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// src/controllers/ui/init.ui-controller.ts
|
|
960
|
+
async function initUiController(ctx) {
|
|
961
|
+
const organizationName = await ctx.prompt("Organization name:");
|
|
962
|
+
const platformName = await ctx.prompt("Platform name:");
|
|
963
|
+
const platformDisplayName = await ctx.prompt("Platform display name:");
|
|
964
|
+
await initService({ organizationName, platformName, platformDisplayName }, ctx);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// src/services/configure-idp.service.ts
|
|
968
|
+
async function configureIdpService(params, logger) {
|
|
969
|
+
await configureIdp(params, logger);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// src/controllers/ui/configure-idp.ui-controller.ts
|
|
973
|
+
async function configureIdpUiController(ctx) {
|
|
974
|
+
const providers = getAllProviders();
|
|
975
|
+
const options = providers.map((p, i) => `${i + 1}: ${p.displayName}`).join(", ");
|
|
976
|
+
const selectionInput = await ctx.prompt(`Select IDP type (${options}):`);
|
|
977
|
+
const index = parseInt(selectionInput.trim(), 10) - 1;
|
|
978
|
+
if (isNaN(index) || index < 0 || index >= providers.length) {
|
|
979
|
+
ctx.log(`Error: Invalid selection "${selectionInput}"`);
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
const provider = providers[index];
|
|
983
|
+
const name = await ctx.prompt("Provider name:");
|
|
984
|
+
const issuer = await ctx.prompt("Issuer URL:");
|
|
985
|
+
const clientId = await ctx.prompt("Client ID:");
|
|
986
|
+
const clientSecret = await ctx.prompt("Client Secret:");
|
|
987
|
+
const extras = {};
|
|
988
|
+
for (const field of provider.fields) {
|
|
989
|
+
extras[field.key] = await ctx.prompt(field.prompt);
|
|
990
|
+
}
|
|
991
|
+
await configureIdpService(
|
|
992
|
+
{ providerType: provider.type, name, issuer, clientId, clientSecret, extras },
|
|
993
|
+
ctx
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// src/controllers/ui/registry.ts
|
|
998
|
+
var uiControllers = /* @__PURE__ */ new Map([
|
|
999
|
+
[CREATE_APPLICATION_COMMAND_NAME, createApplicationUiController],
|
|
1000
|
+
[INIT_COMMAND_NAME, initUiController],
|
|
1001
|
+
[CONFIGURE_IDP_COMMAND_NAME, configureIdpUiController]
|
|
1002
|
+
]);
|
|
1003
|
+
|
|
1004
|
+
// src/hooks/use-command-runner.ts
|
|
1005
|
+
function useCommandRunner({ appendStaticItem, setState }) {
|
|
1006
|
+
const outputCountRef = useRef(0);
|
|
1007
|
+
const abortControllerRef = useRef(null);
|
|
1008
|
+
const promptResolveRef = useRef(null);
|
|
1009
|
+
const [promptMessage, setPromptMessage] = useState2("");
|
|
1010
|
+
const [promptValue, setPromptValue] = useState2("");
|
|
1011
|
+
const abortExecution = useCallback(() => {
|
|
1012
|
+
abortControllerRef.current?.abort();
|
|
1013
|
+
abortControllerRef.current = null;
|
|
1014
|
+
promptResolveRef.current = null;
|
|
1015
|
+
}, []);
|
|
1016
|
+
const runCommand = useCallback(
|
|
1017
|
+
(cmd) => {
|
|
1018
|
+
const controller = new AbortController();
|
|
1019
|
+
abortControllerRef.current = controller;
|
|
1020
|
+
setState(APP_STATE.EXECUTING);
|
|
1021
|
+
const ctx = {
|
|
1022
|
+
signal: controller.signal,
|
|
1023
|
+
log(message) {
|
|
1024
|
+
if (!controller.signal.aborted) {
|
|
1025
|
+
const id = `output-${outputCountRef.current++}`;
|
|
1026
|
+
appendStaticItem({ kind: "output", id, line: message });
|
|
1027
|
+
}
|
|
1028
|
+
},
|
|
1029
|
+
prompt(message) {
|
|
1030
|
+
return new Promise((resolve, reject) => {
|
|
1031
|
+
if (controller.signal.aborted) {
|
|
1032
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
setPromptMessage(message);
|
|
1036
|
+
setPromptValue("");
|
|
1037
|
+
promptResolveRef.current = resolve;
|
|
1038
|
+
setState(APP_STATE.PROMPTING);
|
|
1039
|
+
controller.signal.addEventListener(
|
|
1040
|
+
"abort",
|
|
1041
|
+
() => reject(new DOMException("Aborted", "AbortError")),
|
|
1042
|
+
{ once: true }
|
|
1043
|
+
);
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
const uiController = uiControllers.get(cmd.name);
|
|
1048
|
+
if (!uiController) {
|
|
1049
|
+
const id = `output-${outputCountRef.current++}`;
|
|
1050
|
+
appendStaticItem({ kind: "output", id, line: `Error: No controller found for "${cmd.name}"` });
|
|
1051
|
+
setState(APP_STATE.IDLE);
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
uiController(ctx).then(() => {
|
|
1055
|
+
if (!controller.signal.aborted) {
|
|
1056
|
+
setState(APP_STATE.IDLE);
|
|
1057
|
+
}
|
|
1058
|
+
}).catch((err) => {
|
|
1059
|
+
if (controller.signal.aborted) return;
|
|
1060
|
+
const id = `output-${outputCountRef.current++}`;
|
|
1061
|
+
appendStaticItem({ kind: "output", id, line: `Error: ${formatError(err)}` });
|
|
1062
|
+
setState(APP_STATE.IDLE);
|
|
1063
|
+
});
|
|
1064
|
+
},
|
|
1065
|
+
[appendStaticItem, setState]
|
|
1066
|
+
);
|
|
1067
|
+
const handlePromptSubmit = useCallback(
|
|
1068
|
+
(value) => {
|
|
1069
|
+
const resolve = promptResolveRef.current;
|
|
1070
|
+
promptResolveRef.current = null;
|
|
1071
|
+
setState(APP_STATE.EXECUTING);
|
|
1072
|
+
resolve?.(value);
|
|
1073
|
+
},
|
|
1074
|
+
[setState]
|
|
1075
|
+
);
|
|
1076
|
+
return {
|
|
1077
|
+
runCommand,
|
|
1078
|
+
handlePromptSubmit,
|
|
1079
|
+
abortExecution,
|
|
1080
|
+
promptMessage,
|
|
1081
|
+
promptValue,
|
|
1082
|
+
setPromptValue
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// src/app.tsx
|
|
1087
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1088
|
+
var require2 = createRequire(import.meta.url);
|
|
1089
|
+
var { version } = require2("../package.json");
|
|
1090
|
+
function App() {
|
|
1091
|
+
const { exit } = useApp();
|
|
1092
|
+
const [state, setState] = useState3(APP_STATE.IDLE);
|
|
1093
|
+
const [inputValue, setInputValue] = useState3("");
|
|
1094
|
+
const [selectedIndex, setSelectedIndex] = useState3(0);
|
|
1095
|
+
const [staticItems, setStaticItems] = useState3([{ kind: "banner", id: "banner" }]);
|
|
1096
|
+
const appendStaticItem = useCallback2((item) => {
|
|
1097
|
+
setStaticItems((prev) => [...prev, item]);
|
|
1098
|
+
}, []);
|
|
1099
|
+
const { runCommand, handlePromptSubmit, abortExecution, promptMessage, promptValue, setPromptValue } = useCommandRunner({ appendStaticItem, setState });
|
|
1100
|
+
const query = inputValue.startsWith("/") ? inputValue.slice(1) : "";
|
|
1101
|
+
const filteredCommands = registry.search(query);
|
|
1102
|
+
useInput(
|
|
1103
|
+
(input, key) => {
|
|
1104
|
+
if (key.ctrl && input === "c") {
|
|
1105
|
+
exit();
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
if ((state === APP_STATE.EXECUTING || state === APP_STATE.PROMPTING) && key.escape) {
|
|
1109
|
+
abortExecution();
|
|
1110
|
+
setState(APP_STATE.IDLE);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if (state === APP_STATE.PALETTE) {
|
|
1114
|
+
if (key.escape) {
|
|
1115
|
+
setState(APP_STATE.IDLE);
|
|
1116
|
+
setInputValue("");
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
if (key.upArrow) {
|
|
1120
|
+
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if (key.downArrow) {
|
|
1124
|
+
setSelectedIndex((prev) => Math.min(filteredCommands.length - 1, prev + 1));
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
if (key.return) {
|
|
1128
|
+
const cmd = filteredCommands[selectedIndex];
|
|
1129
|
+
if (cmd) {
|
|
1130
|
+
setInputValue("");
|
|
1131
|
+
runCommand(cmd);
|
|
1132
|
+
}
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
if (key.backspace || key.delete) {
|
|
1136
|
+
setInputValue((prev) => {
|
|
1137
|
+
const next = prev.slice(0, -1);
|
|
1138
|
+
if (!next.startsWith("/")) {
|
|
1139
|
+
setState(APP_STATE.IDLE);
|
|
1140
|
+
return "";
|
|
1141
|
+
}
|
|
1142
|
+
return next;
|
|
1143
|
+
});
|
|
1144
|
+
setSelectedIndex(0);
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
if (input && !key.ctrl && !key.meta) {
|
|
1148
|
+
setInputValue((prev) => prev + input);
|
|
1149
|
+
setSelectedIndex(0);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
},
|
|
1153
|
+
{ isActive: true }
|
|
1154
|
+
);
|
|
1155
|
+
const handleIdleInputChange = useCallback2((value) => {
|
|
1156
|
+
setInputValue(value);
|
|
1157
|
+
if (value.startsWith("/")) {
|
|
1158
|
+
setState(APP_STATE.PALETTE);
|
|
1159
|
+
setSelectedIndex(0);
|
|
1160
|
+
}
|
|
1161
|
+
}, []);
|
|
1162
|
+
const handleIdleSubmit = useCallback2(
|
|
1163
|
+
(value) => {
|
|
1164
|
+
if (!value.startsWith("/")) return;
|
|
1165
|
+
const name = value.slice(1).trim();
|
|
1166
|
+
const cmd = registry.get(name);
|
|
1167
|
+
if (cmd) runCommand(cmd);
|
|
1168
|
+
},
|
|
1169
|
+
[runCommand]
|
|
1170
|
+
);
|
|
1171
|
+
function renderActiveArea() {
|
|
1172
|
+
switch (state) {
|
|
1173
|
+
case APP_STATE.EXECUTING:
|
|
1174
|
+
return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "row", gap: 1, children: [
|
|
1175
|
+
/* @__PURE__ */ jsx6(Spinner, { label: "Running\u2026" }),
|
|
1176
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "(esc to cancel)" })
|
|
1177
|
+
] });
|
|
1178
|
+
case APP_STATE.PROMPTING:
|
|
1179
|
+
return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
|
|
1180
|
+
/* @__PURE__ */ jsx6(Text6, { color: "cyan", children: promptMessage }),
|
|
1181
|
+
/* @__PURE__ */ jsx6(Prompt, { value: promptValue, onChange: setPromptValue, onSubmit: handlePromptSubmit })
|
|
1182
|
+
] });
|
|
1183
|
+
default:
|
|
1184
|
+
return /* @__PURE__ */ jsx6(
|
|
1185
|
+
Prompt,
|
|
1186
|
+
{
|
|
1187
|
+
value: inputValue,
|
|
1188
|
+
onChange: state === APP_STATE.IDLE ? handleIdleInputChange : () => {
|
|
1189
|
+
},
|
|
1190
|
+
onSubmit: state === APP_STATE.IDLE ? handleIdleSubmit : () => {
|
|
1191
|
+
},
|
|
1192
|
+
disabled: state === APP_STATE.PALETTE
|
|
1193
|
+
}
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
|
|
1198
|
+
/* @__PURE__ */ jsx6(ScrollbackHistory, { items: staticItems, version }),
|
|
1199
|
+
renderActiveArea(),
|
|
1200
|
+
state === APP_STATE.PALETTE && /* @__PURE__ */ jsx6(CommandPalette, { commands: filteredCommands, selectedIndex })
|
|
1201
|
+
] });
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// src/controllers/cli/create-application.cli-controller.ts
|
|
1205
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1206
|
+
import { join as join18 } from "path";
|
|
1207
|
+
import { cwd as cwd5 } from "process";
|
|
1208
|
+
async function createApplicationCliController(args2) {
|
|
1209
|
+
const {
|
|
1210
|
+
applicationName,
|
|
1211
|
+
applicationDisplayName,
|
|
1212
|
+
applicationDescription,
|
|
1213
|
+
hasUserInterface,
|
|
1214
|
+
hasBackendService
|
|
1215
|
+
} = args2;
|
|
1216
|
+
if (!applicationName || !applicationDisplayName || !applicationDescription) {
|
|
1217
|
+
console.error("Error: applicationName, applicationDisplayName, and applicationDescription are required.");
|
|
1218
|
+
process.exit(1);
|
|
1219
|
+
}
|
|
1220
|
+
if (!/^[a-z0-9-]+$/.test(applicationName)) {
|
|
1221
|
+
console.error(`Error: Application name "${applicationName}" is invalid. Use only lowercase letters, numbers, and hyphens.`);
|
|
1222
|
+
process.exit(1);
|
|
1223
|
+
}
|
|
1224
|
+
let organizationName;
|
|
1225
|
+
let platformName;
|
|
1226
|
+
try {
|
|
1227
|
+
const corePackageJson = JSON.parse(
|
|
1228
|
+
await readFile9(join18(cwd5(), "core", "package.json"), "utf-8")
|
|
1229
|
+
);
|
|
1230
|
+
const scopeMatch = corePackageJson.name.match(/^@([^/]+)\//);
|
|
1231
|
+
organizationName = scopeMatch?.[1];
|
|
1232
|
+
const rawName = corePackageJson.name.replace(/^@[^/]+\//, "").replace(/-core$/, "");
|
|
1233
|
+
platformName = rawName;
|
|
1234
|
+
if (!organizationName || !platformName) {
|
|
1235
|
+
throw new Error(`Could not parse organization/platform from package name: "${corePackageJson.name}"`);
|
|
1236
|
+
}
|
|
1237
|
+
} catch (err) {
|
|
1238
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1239
|
+
console.error(`Error: Could not read core/package.json \u2014 ${message}`);
|
|
1240
|
+
process.exit(1);
|
|
1241
|
+
}
|
|
1242
|
+
await createApplicationService(
|
|
1243
|
+
{
|
|
1244
|
+
organizationName,
|
|
1245
|
+
platformName,
|
|
1246
|
+
applicationName,
|
|
1247
|
+
applicationDisplayName,
|
|
1248
|
+
applicationDescription,
|
|
1249
|
+
hasUserInterface: hasUserInterface === "yes",
|
|
1250
|
+
hasBackendService: hasBackendService === "yes"
|
|
1251
|
+
},
|
|
1252
|
+
{ log: console.log }
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// src/controllers/cli/init.cli-controller.ts
|
|
1257
|
+
async function initCliController(args2) {
|
|
1258
|
+
const { organizationName, platformName, platformDisplayName } = args2;
|
|
1259
|
+
if (!organizationName || !platformName || !platformDisplayName) {
|
|
1260
|
+
console.error("Error: organizationName, platformName, and platformDisplayName are required.");
|
|
1261
|
+
process.exit(1);
|
|
1262
|
+
}
|
|
1263
|
+
await initService(
|
|
1264
|
+
{ organizationName, platformName, platformDisplayName },
|
|
1265
|
+
{ log: console.log }
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// src/controllers/cli/configure-idp.cli-controller.ts
|
|
1270
|
+
async function configureIdpCliController(args2) {
|
|
1271
|
+
const logger = { log: console.log };
|
|
1272
|
+
const { providerType, name, issuer, clientId, clientSecret } = args2;
|
|
1273
|
+
if (!providerType || !name || !issuer || !clientId || !clientSecret) {
|
|
1274
|
+
logger.log("Error: Missing required arguments: providerType, name, issuer, clientId, clientSecret");
|
|
1275
|
+
process.exit(1);
|
|
1276
|
+
}
|
|
1277
|
+
const provider = idpProviderRegistry.get(providerType);
|
|
1278
|
+
if (!provider) {
|
|
1279
|
+
logger.log(`Error: Unknown IDP type "${providerType}"`);
|
|
1280
|
+
process.exit(1);
|
|
1281
|
+
}
|
|
1282
|
+
const extras = {};
|
|
1283
|
+
for (const field of provider.fields) {
|
|
1284
|
+
const value = args2[field.key];
|
|
1285
|
+
if (!value) {
|
|
1286
|
+
logger.log(`Error: Missing required argument for ${providerType}: ${field.key}`);
|
|
1287
|
+
process.exit(1);
|
|
1288
|
+
}
|
|
1289
|
+
extras[field.key] = value;
|
|
1290
|
+
}
|
|
1291
|
+
await configureIdpService({ providerType, name, issuer, clientId, clientSecret, extras }, logger);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// src/utils/run-npm-script.ts
|
|
1295
|
+
import { spawn } from "child_process";
|
|
1296
|
+
import { access } from "fs/promises";
|
|
1297
|
+
import { join as join19 } from "path";
|
|
1298
|
+
async function runNpmScript(scriptName, logger, signal) {
|
|
1299
|
+
const localDir = join19(process.cwd(), "local");
|
|
1300
|
+
try {
|
|
1301
|
+
await access(localDir);
|
|
1302
|
+
} catch {
|
|
1303
|
+
logger.log(`Error: "local/" directory not found. Run "platform init" first.`);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
return new Promise((resolve) => {
|
|
1307
|
+
const child = spawn("npm", ["run", scriptName], {
|
|
1308
|
+
cwd: localDir,
|
|
1309
|
+
shell: true,
|
|
1310
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1311
|
+
});
|
|
1312
|
+
const onAbort = () => {
|
|
1313
|
+
child.kill("SIGTERM");
|
|
1314
|
+
};
|
|
1315
|
+
if (signal) {
|
|
1316
|
+
if (signal.aborted) {
|
|
1317
|
+
child.kill("SIGTERM");
|
|
1318
|
+
resolve();
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1322
|
+
}
|
|
1323
|
+
child.stdout.on("data", (data) => {
|
|
1324
|
+
for (const line of data.toString().split("\n")) {
|
|
1325
|
+
if (line.trim()) logger.log(line);
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
child.stderr.on("data", (data) => {
|
|
1329
|
+
for (const line of data.toString().split("\n")) {
|
|
1330
|
+
if (line.trim()) logger.log(line);
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
child.on("close", (code, sig) => {
|
|
1334
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1335
|
+
if (sig === "SIGTERM" || signal?.aborted) {
|
|
1336
|
+
logger.log(`Command cancelled.`);
|
|
1337
|
+
} else if (code !== 0) {
|
|
1338
|
+
logger.log(`Command "npm run ${scriptName}" exited with code ${code}.`);
|
|
1339
|
+
}
|
|
1340
|
+
resolve();
|
|
1341
|
+
});
|
|
1342
|
+
child.on("error", (err) => {
|
|
1343
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1344
|
+
logger.log(`Failed to run "npm run ${scriptName}": ${err.message}`);
|
|
1345
|
+
resolve();
|
|
1346
|
+
});
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// src/commands/local-scripts/local-script.command.ts
|
|
1351
|
+
var INSTALL_COMMAND_NAME = "install";
|
|
1352
|
+
var BUILD_COMMAND_NAME = "build";
|
|
1353
|
+
var START_COMMAND_NAME = "start";
|
|
1354
|
+
var STOP_COMMAND_NAME = "stop";
|
|
1355
|
+
var DESTROY_COMMAND_NAME = "destroy";
|
|
1356
|
+
async function runLocalScript(scriptName, logger, signal) {
|
|
1357
|
+
await runNpmScript(scriptName, logger, signal);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// src/services/local-script.service.ts
|
|
1361
|
+
async function localScriptService(scriptName, logger, signal) {
|
|
1362
|
+
await runLocalScript(scriptName, logger, signal);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// src/utils/cli-spinner.ts
|
|
1366
|
+
var FRAMES2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1367
|
+
var INTERVAL_MS2 = 80;
|
|
1368
|
+
function createCliSpinner(label) {
|
|
1369
|
+
let frame = 0;
|
|
1370
|
+
let intervalId = null;
|
|
1371
|
+
function render2() {
|
|
1372
|
+
process.stdout.write(`\r${FRAMES2[frame]} ${label}`);
|
|
1373
|
+
frame = (frame + 1) % FRAMES2.length;
|
|
1374
|
+
}
|
|
1375
|
+
function clearLine() {
|
|
1376
|
+
process.stdout.write("\r\x1B[K");
|
|
1377
|
+
}
|
|
1378
|
+
return {
|
|
1379
|
+
start() {
|
|
1380
|
+
render2();
|
|
1381
|
+
intervalId = setInterval(render2, INTERVAL_MS2);
|
|
1382
|
+
},
|
|
1383
|
+
log(message) {
|
|
1384
|
+
clearLine();
|
|
1385
|
+
console.log(message);
|
|
1386
|
+
render2();
|
|
1387
|
+
},
|
|
1388
|
+
stop() {
|
|
1389
|
+
if (intervalId) {
|
|
1390
|
+
clearInterval(intervalId);
|
|
1391
|
+
intervalId = null;
|
|
1392
|
+
}
|
|
1393
|
+
clearLine();
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// src/controllers/cli/local-script.cli-controller.ts
|
|
1399
|
+
function createLocalScriptCliController(scriptName) {
|
|
1400
|
+
return async (_args) => {
|
|
1401
|
+
const spinner = createCliSpinner(`Running ${scriptName}\u2026`);
|
|
1402
|
+
spinner.start();
|
|
1403
|
+
await localScriptService(scriptName, { log: (msg) => spinner.log(msg) });
|
|
1404
|
+
spinner.stop();
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/controllers/cli/registry.ts
|
|
1409
|
+
var cliControllers = /* @__PURE__ */ new Map([
|
|
1410
|
+
[CREATE_APPLICATION_COMMAND_NAME, createApplicationCliController],
|
|
1411
|
+
[INIT_COMMAND_NAME, initCliController],
|
|
1412
|
+
[CONFIGURE_IDP_COMMAND_NAME, configureIdpCliController],
|
|
1413
|
+
[INSTALL_COMMAND_NAME, createLocalScriptCliController(INSTALL_COMMAND_NAME)],
|
|
1414
|
+
[BUILD_COMMAND_NAME, createLocalScriptCliController(BUILD_COMMAND_NAME)],
|
|
1415
|
+
[START_COMMAND_NAME, createLocalScriptCliController(START_COMMAND_NAME)],
|
|
1416
|
+
[STOP_COMMAND_NAME, createLocalScriptCliController(STOP_COMMAND_NAME)],
|
|
1417
|
+
[DESTROY_COMMAND_NAME, createLocalScriptCliController(DESTROY_COMMAND_NAME)]
|
|
1418
|
+
]);
|
|
1419
|
+
|
|
1420
|
+
// src/utils/parse-args.ts
|
|
1421
|
+
function parseKeyValueArgs(args2) {
|
|
1422
|
+
const result = {};
|
|
1423
|
+
for (const arg of args2) {
|
|
1424
|
+
const eqIndex = arg.indexOf("=");
|
|
1425
|
+
if (eqIndex > 0) {
|
|
1426
|
+
const key = arg.slice(0, eqIndex);
|
|
1427
|
+
const value = arg.slice(eqIndex + 1);
|
|
1428
|
+
result[key] = value;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
return result;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// src/index.tsx
|
|
1435
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
1436
|
+
var args = process.argv.slice(2);
|
|
1437
|
+
if (args.length === 0) {
|
|
1438
|
+
render(/* @__PURE__ */ jsx7(App, {}));
|
|
1439
|
+
} else {
|
|
1440
|
+
const commandName = args[0];
|
|
1441
|
+
const params = parseKeyValueArgs(args.slice(1));
|
|
1442
|
+
const controller = cliControllers.get(commandName);
|
|
1443
|
+
if (!controller) {
|
|
1444
|
+
console.error(`Unknown command: ${commandName}`);
|
|
1445
|
+
process.exit(1);
|
|
1446
|
+
}
|
|
1447
|
+
await controller(params);
|
|
1448
|
+
}
|