@jtl-software/create-cloud-app 0.0.23 → 0.0.25
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/README.md +120 -14
- package/dist/index.js +22 -25
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,42 +1,148 @@
|
|
|
1
1
|
# @jtl-software/create-cloud-app
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CLI for [JTL Platform](https://www.jtl-software.com/) cloud apps. Scaffolds a new app project and registers/updates its manifest against the cloud.
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
7
7
|
Node.js **24** (current LTS) or newer. The scaffolded project sets the same floor via `engines.node`.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
The CLI has two entry points:
|
|
12
|
+
|
|
13
|
+
- The default invocation (no arguments) scaffolds a new project.
|
|
14
|
+
- `register` registers or updates a manifest against the JTL Cloud.
|
|
15
|
+
|
|
16
|
+
### `npm create @jtl-software/cloud-app@latest`
|
|
17
|
+
|
|
18
|
+
Scaffold a new app interactively.
|
|
10
19
|
|
|
11
20
|
```bash
|
|
12
21
|
npm create @jtl-software/cloud-app@latest
|
|
13
22
|
```
|
|
14
23
|
|
|
15
|
-
|
|
24
|
+
You'll be prompted for:
|
|
16
25
|
|
|
17
|
-
- **App name
|
|
18
|
-
- **Description
|
|
19
|
-
- **Backend
|
|
20
|
-
- **Frontend
|
|
26
|
+
- **App name**: directory name, package name, and manifest identifier
|
|
27
|
+
- **Description**: placed in the app manifest
|
|
28
|
+
- **Backend**: Node.js (Express + TypeScript) or .NET (ASP.NET Core + FastEndpoints)
|
|
29
|
+
- **Frontend**: React (Vite + Tailwind + JTL Platform UI)
|
|
21
30
|
|
|
22
|
-
Then
|
|
31
|
+
Then:
|
|
23
32
|
|
|
24
33
|
```bash
|
|
25
34
|
cd my-app
|
|
26
35
|
npm install
|
|
36
|
+
npm run register
|
|
27
37
|
npm run dev
|
|
28
38
|
```
|
|
29
39
|
|
|
30
|
-
|
|
40
|
+
The generated project includes:
|
|
41
|
+
|
|
42
|
+
- A monorepo wired up with [Turborepo](https://turbo.build/)
|
|
43
|
+
- A frontend with welcome pages explaining each app mode and manifest mapping
|
|
44
|
+
- A backend with JWT verification, tenant connection, and ERP API proxy
|
|
45
|
+
- A ready-to-register `manifest.json`
|
|
46
|
+
- An `npm run register` script that delegates to `npx -y @jtl-software/create-cloud-app@latest register`
|
|
47
|
+
|
|
48
|
+
### `register`
|
|
49
|
+
|
|
50
|
+
Register a new app or update an existing app's manifest. Reads `manifest.json` from the current working directory and pushes it to the App Service.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx @jtl-software/create-cloud-app register
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
By default the command is interactive: it opens a browser for OAuth login, then prompts for tenant and whether to update an existing app or create a new one.
|
|
57
|
+
|
|
58
|
+
Flags:
|
|
59
|
+
|
|
60
|
+
| Flag | Description |
|
|
61
|
+
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
62
|
+
| `--api-host <url>` | API host. Default `https://api.jtl-cloud.com`. |
|
|
63
|
+
| `--auth-host <url>` | OAuth host. Default `https://auth.jtl-cloud.com`. The CLI picks the matching client ID for the known prod/QA/dev hosts. |
|
|
64
|
+
| `--hub-host <url>` | Hub host (used in printed links). Default `https://hub.jtl-cloud.com`. |
|
|
65
|
+
| `--client-id <id>` | Override the OAuth client ID. Only needed for custom auth hosts. |
|
|
66
|
+
| `--scope <scope>` | OAuth scope. Default `openid offline_access`. |
|
|
67
|
+
| `--reauth` | Skip the cached token and open the browser again. Useful after switching accounts. |
|
|
68
|
+
| `--yes`, `-y` | Non-interactive mode. Auto-confirms prompts and fails if any choice is ambiguous (e.g. multiple tenants match). Required for CI. |
|
|
69
|
+
| `--tenant <id-or-slug>` | Select a tenant by ID or slug (case-insensitive). |
|
|
70
|
+
| `--existing-app-id <id>` | Update the app with this ID instead of prompting. Errors if it doesn't match this manifest's technical name. |
|
|
71
|
+
| `--new` | Force-create a new app even if a matching one exists. Mutually exclusive with `--existing-app-id`. |
|
|
72
|
+
| `--on-collision <overwrite\|print\|cancel>` | What to do when an existing credentials file would be overwritten during provisioning. `print` writes the secret to stdout instead of the file. Required in `--yes` mode if a collision is possible. |
|
|
73
|
+
|
|
74
|
+
Tokens are cached under `~/.config/jtl-cli/tokens.json` (mode `0600`). Override the path with the `JTL_CLI_TOKEN_CACHE_FILE` environment variable.
|
|
75
|
+
|
|
76
|
+
## CI usage: keep your deployed manifest in sync with the repo
|
|
77
|
+
|
|
78
|
+
A common setup is to commit `manifest.json` to source control and have CI push it to the cloud whenever it changes, so the deployed app always matches what's on `main`.
|
|
79
|
+
|
|
80
|
+
The `register` command supports this. You need three things:
|
|
81
|
+
|
|
82
|
+
1. **Pin the tenant and app ID** so the run is deterministic.
|
|
83
|
+
2. **Run with `--yes`** so prompts don't block.
|
|
84
|
+
3. **Seed the OAuth token cache** so login doesn't open a browser.
|
|
85
|
+
|
|
86
|
+
### Step 1: capture a refresh token locally
|
|
87
|
+
|
|
88
|
+
Run `register` once on your machine while signed in as the user (or service account) you want CI to act as:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
cd path/to/your/app
|
|
92
|
+
npx @jtl-software/create-cloud-app register --scope "openid offline_access"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
After it completes, `~/.config/jtl-cli/tokens.json` contains an entry with a `refresh_token`. Copy that file's contents and store it as a CI secret (e.g. `JTL_CLI_TOKENS`).
|
|
96
|
+
|
|
97
|
+
The cached token will be silently refreshed on every CI run, so it stays valid as long as CI runs often enough that the refresh token doesn't expire.
|
|
98
|
+
|
|
99
|
+
### Step 2: look up your tenant and app ID
|
|
100
|
+
|
|
101
|
+
You can read them off the previous interactive run (the CLI prints both). The tenant slug is the stable, human-friendly identifier. The app ID is a UUID printed in the "Review" section.
|
|
102
|
+
|
|
103
|
+
### Step 3: CI workflow
|
|
104
|
+
|
|
105
|
+
GitHub Actions example:
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
name: Update manifest
|
|
109
|
+
|
|
110
|
+
on:
|
|
111
|
+
push:
|
|
112
|
+
branches: [main]
|
|
113
|
+
paths:
|
|
114
|
+
- manifest.json
|
|
115
|
+
|
|
116
|
+
jobs:
|
|
117
|
+
register:
|
|
118
|
+
runs-on: ubuntu-latest
|
|
119
|
+
steps:
|
|
120
|
+
- uses: actions/checkout@v4
|
|
121
|
+
- uses: actions/setup-node@v4
|
|
122
|
+
with:
|
|
123
|
+
node-version: 24
|
|
124
|
+
- name: Write token cache
|
|
125
|
+
env:
|
|
126
|
+
JTL_CLI_TOKENS: ${{ secrets.JTL_CLI_TOKENS }}
|
|
127
|
+
run: |
|
|
128
|
+
mkdir -p "$RUNNER_TEMP/jtl-cli"
|
|
129
|
+
printf '%s' "$JTL_CLI_TOKENS" > "$RUNNER_TEMP/jtl-cli/tokens.json"
|
|
130
|
+
chmod 600 "$RUNNER_TEMP/jtl-cli/tokens.json"
|
|
131
|
+
- name: Update manifest
|
|
132
|
+
env:
|
|
133
|
+
JTL_CLI_TOKEN_CACHE_FILE: ${{ runner.temp }}/jtl-cli/tokens.json
|
|
134
|
+
run: |
|
|
135
|
+
npx -y @jtl-software/create-cloud-app@latest register \
|
|
136
|
+
--yes \
|
|
137
|
+
--tenant my-tenant-slug \
|
|
138
|
+
--existing-app-id 00000000-0000-0000-0000-000000000000
|
|
139
|
+
```
|
|
31
140
|
|
|
32
|
-
-
|
|
33
|
-
- Frontend with welcome pages explaining each app mode and manifest mapping
|
|
34
|
-
- Backend with JWT verification, tenant connection, and ERP API proxy
|
|
35
|
-
- Ready-to-register `manifest.json`
|
|
141
|
+
`--yes` keeps the run non-interactive. `--tenant` and `--existing-app-id` pin the target so the command can't accidentally touch a different tenant or app. The job only runs when `manifest.json` changes, so the deployed manifest tracks `main`.
|
|
36
142
|
|
|
37
143
|
## After scaffolding
|
|
38
144
|
|
|
39
|
-
1. Register your manifest in the [Partner Portal](https://partner.jtl-cloud.com/)
|
|
145
|
+
1. Register your manifest in the [Partner Portal](https://partner.jtl-cloud.com/) or with `npm run register`
|
|
40
146
|
2. Add your Client ID and Secret to the backend config
|
|
41
147
|
3. Install the app from [JTL-Cloud Hub](https://hub.jtl-cloud.com/) under "Apps in development"
|
|
42
148
|
|
package/dist/index.js
CHANGED
|
@@ -999,6 +999,22 @@ function buildShortDescription(description) {
|
|
|
999
999
|
if (description.length <= SHORT_DESCRIPTION_MAX_LENGTH) return description;
|
|
1000
1000
|
return description.slice(0, SHORT_DESCRIPTION_MAX_LENGTH - 1).trimEnd() + "\u2026";
|
|
1001
1001
|
}
|
|
1002
|
+
function buildPlaceholders(opts) {
|
|
1003
|
+
return {
|
|
1004
|
+
"{{APP_NAME}}": opts.appName,
|
|
1005
|
+
"{{APP_NAME_PASCAL}}": toPascalCase(opts.appName),
|
|
1006
|
+
"{{APP_TECHNICAL_NAME}}": buildTechnicalName(opts.appName),
|
|
1007
|
+
"{{APP_DESCRIPTION}}": opts.description,
|
|
1008
|
+
"{{APP_DESCRIPTION_SHORT}}": buildShortDescription(opts.description)
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
function applyPlaceholders(content, replacements) {
|
|
1012
|
+
let result = content;
|
|
1013
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
1014
|
+
result = result.replaceAll(key, value);
|
|
1015
|
+
}
|
|
1016
|
+
return result;
|
|
1017
|
+
}
|
|
1002
1018
|
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1003
1019
|
".png",
|
|
1004
1020
|
".jpg",
|
|
@@ -1022,10 +1038,7 @@ function transformPackageJson(content, appName, templateType, replacements) {
|
|
|
1022
1038
|
for (const field of PACKAGE_JSON_STRIP_FIELDS) {
|
|
1023
1039
|
delete pkg[field];
|
|
1024
1040
|
}
|
|
1025
|
-
|
|
1026
|
-
for (const [key, value] of Object.entries(replacements)) {
|
|
1027
|
-
result = result.replaceAll(key, value);
|
|
1028
|
-
}
|
|
1041
|
+
const result = applyPlaceholders(JSON.stringify(pkg, null, 2), replacements);
|
|
1029
1042
|
return result + "\n";
|
|
1030
1043
|
}
|
|
1031
1044
|
function copyDir(src, dest, replacements, templateType, appName) {
|
|
@@ -1036,9 +1049,7 @@ function copyDir(src, dest, replacements, templateType, appName) {
|
|
|
1036
1049
|
if (destName.startsWith("_git")) {
|
|
1037
1050
|
destName = `.${destName.slice(1)}`;
|
|
1038
1051
|
}
|
|
1039
|
-
|
|
1040
|
-
destName = destName.replaceAll(key, value);
|
|
1041
|
-
}
|
|
1052
|
+
destName = applyPlaceholders(destName, replacements);
|
|
1042
1053
|
const destPath = path2.join(dest, destName);
|
|
1043
1054
|
if (entry.isDirectory()) {
|
|
1044
1055
|
copyDir(srcPath, destPath, replacements);
|
|
@@ -1053,11 +1064,8 @@ function copyDir(src, dest, replacements, templateType, appName) {
|
|
|
1053
1064
|
if (BINARY_EXTENSIONS.has(ext)) {
|
|
1054
1065
|
fs2.copyFileSync(srcPath, destPath);
|
|
1055
1066
|
} else {
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
content = content.replaceAll(key, value);
|
|
1059
|
-
}
|
|
1060
|
-
fs2.writeFileSync(destPath, content);
|
|
1067
|
+
const content = fs2.readFileSync(srcPath, "utf-8");
|
|
1068
|
+
fs2.writeFileSync(destPath, applyPlaceholders(content, replacements));
|
|
1061
1069
|
}
|
|
1062
1070
|
}
|
|
1063
1071
|
}
|
|
@@ -1066,11 +1074,7 @@ function buildReplacements(base, template) {
|
|
|
1066
1074
|
const extra = template.meta.extraReplacements ?? {};
|
|
1067
1075
|
const resolved = {};
|
|
1068
1076
|
for (const [key, value] of Object.entries(extra)) {
|
|
1069
|
-
|
|
1070
|
-
for (const [baseKey, baseValue] of Object.entries(base)) {
|
|
1071
|
-
resolvedValue = resolvedValue.replaceAll(baseKey, baseValue);
|
|
1072
|
-
}
|
|
1073
|
-
resolved[key] = resolvedValue;
|
|
1077
|
+
resolved[key] = applyPlaceholders(value, base);
|
|
1074
1078
|
}
|
|
1075
1079
|
return { ...base, ...resolved };
|
|
1076
1080
|
}
|
|
@@ -1083,14 +1087,7 @@ async function scaffold({
|
|
|
1083
1087
|
targetDir: customTargetDir
|
|
1084
1088
|
}) {
|
|
1085
1089
|
const targetDir = customTargetDir ?? path2.resolve(process.cwd(), appName);
|
|
1086
|
-
const
|
|
1087
|
-
const baseReplacements = {
|
|
1088
|
-
"{{APP_NAME}}": appName,
|
|
1089
|
-
"{{APP_NAME_PASCAL}}": pascalName,
|
|
1090
|
-
"{{APP_TECHNICAL_NAME}}": buildTechnicalName(appName),
|
|
1091
|
-
"{{APP_DESCRIPTION}}": description,
|
|
1092
|
-
"{{APP_DESCRIPTION_SHORT}}": buildShortDescription(description)
|
|
1093
|
-
};
|
|
1090
|
+
const baseReplacements = buildPlaceholders({ appName, description });
|
|
1094
1091
|
if (fs2.existsSync(targetDir)) {
|
|
1095
1092
|
throw new Error(`Directory "${appName}" already exists`);
|
|
1096
1093
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jtl-software/create-cloud-app",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.25",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI tool for scaffolding JTL Platform cloud apps",
|
|
6
6
|
"bin": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"@clack/prompts": "^1.2.0",
|
|
21
21
|
"@jtl-software/cloud-app-template-backend-dotnet": "0.0.12",
|
|
22
22
|
"@jtl-software/cloud-app-template-backend-node": "0.0.14",
|
|
23
|
+
"@jtl-software/cloud-app-template-backend-php": "0.1.0",
|
|
23
24
|
"@jtl-software/cloud-app-template-frontend-react": "0.0.16",
|
|
24
25
|
"@jtl-software/cloud-app-template-shared": "0.0.6",
|
|
25
26
|
"picocolors": "^1.1.1"
|