@jtl-software/create-cloud-app 0.0.14 → 0.0.16

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 (2) hide show
  1. package/dist/index.js +982 -47
  2. package/package.json +6 -5
package/dist/index.js CHANGED
@@ -1,13 +1,935 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import * as p from "@clack/prompts";
5
- import pc from "picocolors";
4
+ import * as p2 from "@clack/prompts";
5
+ import pc2 from "picocolors";
6
6
 
7
- // src/scaffold.ts
7
+ // src/register.ts
8
+ import * as p from "@clack/prompts";
9
+ import { spawn } from "child_process";
8
10
  import crypto from "crypto";
9
11
  import fs from "fs";
12
+ import http from "http";
13
+ import os from "os";
10
14
  import path from "path";
15
+ import { parseArgs } from "util";
16
+ import pc from "picocolors";
17
+ var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
18
+ var CALLBACK_PORT = 9876;
19
+ var REDIRECT_URI = `http://localhost:${CALLBACK_PORT}/callback`;
20
+ var DEFAULT_SCOPE = "openid offline_access";
21
+ var DEFAULT_API_HOST = "https://api.jtl-cloud.com";
22
+ var DEFAULT_AUTH_HOST = "https://auth.jtl-cloud.com";
23
+ var CLIENT_IDS_BY_AUTH_HOST = {
24
+ "auth.jtl-cloud.com": "b6aca875-c9a3-4010-9fd7-7f8d2e0b0333",
25
+ "auth.dev.jtl-cloud.com": "9c230937-6a52-48d6-9a39-e97edd8ef511",
26
+ "auth.qa.jtl-cloud.com": "fdf8ec1d-b0d3-4f16-9f29-a2ebbbf62951"
27
+ };
28
+ var TOKEN_EXPIRY_MARGIN_S = 60;
29
+ function tokenCacheFile() {
30
+ return process.env.JTL_CLI_TOKEN_CACHE_FILE ?? path.join(os.homedir(), ".config", "jtl-cli", "tokens.json");
31
+ }
32
+ var BailError = class extends Error {
33
+ constructor(message) {
34
+ super(message);
35
+ this.name = "BailError";
36
+ }
37
+ };
38
+ function bail(message) {
39
+ p.cancel(message);
40
+ throw new BailError(message);
41
+ }
42
+ function base64url(buf) {
43
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
44
+ }
45
+ function generatePkce() {
46
+ const verifier = base64url(crypto.randomBytes(32));
47
+ const challenge = base64url(
48
+ crypto.createHash("sha256").update(verifier).digest()
49
+ );
50
+ return { verifier, challenge };
51
+ }
52
+ function parseEnv(content) {
53
+ const result = {};
54
+ for (const line of content.split(/\r?\n/)) {
55
+ const trimmed = line.trim();
56
+ if (!trimmed || trimmed.startsWith("#")) continue;
57
+ const eq = trimmed.indexOf("=");
58
+ if (eq === -1) continue;
59
+ const key = trimmed.slice(0, eq).trim();
60
+ let value = trimmed.slice(eq + 1).trim();
61
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
62
+ value = value.slice(1, -1);
63
+ }
64
+ result[key] = value;
65
+ }
66
+ return result;
67
+ }
68
+ function pickFirstLocaleEntry(entries) {
69
+ if (!entries || typeof entries !== "object") return void 0;
70
+ const keys = Object.keys(entries);
71
+ if (!keys.length) return void 0;
72
+ return entries[keys[0]];
73
+ }
74
+ function summarizeManifest(manifest) {
75
+ const m = manifest.manifest ?? {};
76
+ const listing = manifest.listing ?? {};
77
+ const nameEntry = pickFirstLocaleEntry(listing.name);
78
+ const descEntry = pickFirstLocaleEntry(listing.description);
79
+ return {
80
+ name: nameEntry?.short ?? nameEntry?.full ?? m.technicalName ?? "(unnamed)",
81
+ technicalName: m.technicalName ?? "(unset)",
82
+ version: m.version ?? "(unset)",
83
+ description: descEntry?.short ?? descEntry?.full
84
+ };
85
+ }
86
+ function readEnvFile(filePath) {
87
+ if (!fs.existsSync(filePath)) return {};
88
+ return parseEnv(fs.readFileSync(filePath, "utf-8"));
89
+ }
90
+ function upsertEnv(filePath, updates) {
91
+ const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : "";
92
+ const lines = existing.split(/\r?\n/);
93
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
94
+ const remaining = new Map(Object.entries(updates));
95
+ const next = lines.map((line) => {
96
+ const eq = line.indexOf("=");
97
+ if (eq === -1) return line;
98
+ const key = line.slice(0, eq).trim();
99
+ if (remaining.has(key)) {
100
+ const value = remaining.get(key);
101
+ remaining.delete(key);
102
+ return `${key}=${value}`;
103
+ }
104
+ return line;
105
+ });
106
+ for (const [key, value] of remaining) {
107
+ next.push(`${key}=${value}`);
108
+ }
109
+ fs.writeFileSync(filePath, next.join("\n") + "\n");
110
+ }
111
+ function findDotnetProject(backendDir) {
112
+ if (!fs.existsSync(backendDir)) return null;
113
+ for (const entry of fs.readdirSync(backendDir, { withFileTypes: true })) {
114
+ if (!entry.isDirectory()) continue;
115
+ const exampleFile = path.join(
116
+ backendDir,
117
+ entry.name,
118
+ "appsettings.Local.json.example"
119
+ );
120
+ const csproj = path.join(backendDir, entry.name, `${entry.name}.csproj`);
121
+ if (fs.existsSync(exampleFile) || fs.existsSync(csproj)) {
122
+ return path.join(backendDir, entry.name);
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+ function detectBackend(root) {
128
+ const backendDir = path.join(root, "packages", "backend");
129
+ const envFile = path.join(backendDir, ".env");
130
+ const envExample = path.join(backendDir, ".env.example");
131
+ if (fs.existsSync(envFile) || fs.existsSync(envExample)) {
132
+ return { kind: "node", envFile };
133
+ }
134
+ const dotnetDir = findDotnetProject(backendDir);
135
+ if (dotnetDir) {
136
+ return {
137
+ kind: "dotnet",
138
+ settingsFile: path.join(dotnetDir, "appsettings.Local.json")
139
+ };
140
+ }
141
+ return { kind: "node", envFile: path.join(root, ".env") };
142
+ }
143
+ function readExistingCredentials(backend) {
144
+ if (backend.kind === "node") {
145
+ const values = readEnvFile(backend.envFile);
146
+ return { clientId: values.CLIENT_ID, clientSecret: values.CLIENT_SECRET };
147
+ }
148
+ if (!fs.existsSync(backend.settingsFile)) return {};
149
+ try {
150
+ const settings = JSON.parse(fs.readFileSync(backend.settingsFile, "utf-8"));
151
+ return {
152
+ clientId: settings?.JtlPlatform?.ClientId,
153
+ clientSecret: settings?.JtlPlatform?.ClientSecret
154
+ };
155
+ } catch {
156
+ return {};
157
+ }
158
+ }
159
+ function writeBackendCredentials(backend, clientId, clientSecret) {
160
+ if (backend.kind === "node") {
161
+ upsertEnv(backend.envFile, {
162
+ CLIENT_ID: clientId,
163
+ CLIENT_SECRET: clientSecret
164
+ });
165
+ return backend.envFile;
166
+ }
167
+ let settings = {};
168
+ if (fs.existsSync(backend.settingsFile)) {
169
+ try {
170
+ settings = JSON.parse(fs.readFileSync(backend.settingsFile, "utf-8"));
171
+ } catch {
172
+ settings = {};
173
+ }
174
+ }
175
+ settings.JtlPlatform = {
176
+ ...settings.JtlPlatform ?? {},
177
+ ClientId: clientId,
178
+ ClientSecret: clientSecret
179
+ };
180
+ fs.writeFileSync(
181
+ backend.settingsFile,
182
+ JSON.stringify(settings, null, 2) + "\n"
183
+ );
184
+ return backend.settingsFile;
185
+ }
186
+ function readManifest(manifestPath) {
187
+ if (!fs.existsSync(manifestPath)) {
188
+ bail(
189
+ `manifest.json not found at ${manifestPath}. Run this from the project root.`
190
+ );
191
+ }
192
+ try {
193
+ return JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
194
+ } catch (err) {
195
+ bail(`manifest.json is not valid JSON: ${err.message}`);
196
+ }
197
+ }
198
+ function readTokenCache() {
199
+ const file = tokenCacheFile();
200
+ if (!fs.existsSync(file)) return {};
201
+ try {
202
+ return JSON.parse(fs.readFileSync(file, "utf-8"));
203
+ } catch {
204
+ return {};
205
+ }
206
+ }
207
+ function writeTokenCache(cache) {
208
+ const file = tokenCacheFile();
209
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 448 });
210
+ fs.writeFileSync(file, JSON.stringify(cache, null, 2), { mode: 384 });
211
+ }
212
+ function cacheKey(authBaseUrl) {
213
+ return new URL(authBaseUrl).host;
214
+ }
215
+ function getCachedToken(authBaseUrl, clientId, scope) {
216
+ const entry = readTokenCache()[cacheKey(authBaseUrl)];
217
+ if (!entry || entry.client_id !== clientId) return null;
218
+ if (scope !== void 0 && entry.scope !== void 0 && entry.scope !== scope) {
219
+ return null;
220
+ }
221
+ return entry;
222
+ }
223
+ function saveCachedToken(authBaseUrl, token) {
224
+ const cache = readTokenCache();
225
+ cache[cacheKey(authBaseUrl)] = token;
226
+ writeTokenCache(cache);
227
+ }
228
+ function makeCachedToken(clientId, tokens, fallbackRefreshToken, scope) {
229
+ return {
230
+ client_id: clientId,
231
+ access_token: tokens.access_token,
232
+ expires_at: Math.floor(Date.now() / 1e3) + Math.max(60, tokens.expires_in ?? 3600),
233
+ refresh_token: tokens.refresh_token ?? fallbackRefreshToken,
234
+ scope
235
+ };
236
+ }
237
+ var defaultOpener = (url) => {
238
+ const platform = process.platform;
239
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
240
+ const args = platform === "win32" ? ["", url] : [url];
241
+ try {
242
+ spawn(cmd, args, {
243
+ stdio: "ignore",
244
+ detached: true,
245
+ shell: platform === "win32"
246
+ }).unref();
247
+ } catch {
248
+ }
249
+ };
250
+ function escapeHtml(value) {
251
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
252
+ }
253
+ function callbackPage(title, message, isError = false) {
254
+ return `<!doctype html>
255
+ <html lang="en">
256
+ <head>
257
+ <meta charset="utf-8" />
258
+ <title>JTL Cloud Apps CLI</title>
259
+ <style>
260
+ body { font-family: -apple-system, system-ui, sans-serif; max-width: 480px; margin: 80px auto; padding: 0 24px; color: #1f2937; }
261
+ h1 { font-size: 20px; margin-bottom: 8px; }
262
+ p { color: ${isError ? "#b91c1c" : "#6b7280"}; line-height: 1.5; }
263
+ </style>
264
+ </head>
265
+ <body>
266
+ <h1>${escapeHtml(title)}</h1>
267
+ <p>${escapeHtml(message)}</p>
268
+ </body>
269
+ </html>`;
270
+ }
271
+ function captureAuthCode(authorizeUrl, expectedState, verifier, options = {}) {
272
+ const opener = options.opener ?? defaultOpener;
273
+ const port = options.port ?? CALLBACK_PORT;
274
+ return new Promise((resolve, reject) => {
275
+ let settled = false;
276
+ const closeAndSettle = (res, next) => {
277
+ if (settled) return;
278
+ settled = true;
279
+ const finish = () => server.close(() => next());
280
+ if (res.writableEnded) {
281
+ finish();
282
+ } else {
283
+ res.once("finish", finish);
284
+ }
285
+ };
286
+ const server = http.createServer(
287
+ (req, res) => {
288
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
289
+ if (url.pathname !== "/callback" || req.method !== "GET") {
290
+ res.writeHead(404);
291
+ res.end();
292
+ return;
293
+ }
294
+ const error = url.searchParams.get("error");
295
+ const description = url.searchParams.get("error_description") ?? "";
296
+ const code = url.searchParams.get("code");
297
+ const state = url.searchParams.get("state");
298
+ if (error) {
299
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
300
+ res.end(
301
+ callbackPage("Sign-in failed", `${error}: ${description}`, true)
302
+ );
303
+ closeAndSettle(
304
+ res,
305
+ () => reject(new Error(`${error}: ${description || "no description"}`))
306
+ );
307
+ return;
308
+ }
309
+ if (state !== expectedState) {
310
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
311
+ res.end(callbackPage("Sign-in failed", "state mismatch", true));
312
+ closeAndSettle(res, () => reject(new Error("OAuth state mismatch")));
313
+ return;
314
+ }
315
+ if (!code) {
316
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
317
+ res.end(
318
+ callbackPage(
319
+ "Sign-in failed",
320
+ "no authorization code in response",
321
+ true
322
+ )
323
+ );
324
+ closeAndSettle(
325
+ res,
326
+ () => reject(new Error("OAuth callback missing code"))
327
+ );
328
+ return;
329
+ }
330
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
331
+ res.end(
332
+ callbackPage(
333
+ "Signed in",
334
+ "You can close this window and return to your terminal."
335
+ )
336
+ );
337
+ closeAndSettle(res, () => resolve({ code, verifier }));
338
+ }
339
+ );
340
+ const timer = setTimeout(() => {
341
+ if (settled) return;
342
+ settled = true;
343
+ server.close(
344
+ () => reject(new Error(`login timed out after ${LOGIN_TIMEOUT_MS / 1e3}s`))
345
+ );
346
+ }, LOGIN_TIMEOUT_MS);
347
+ timer.unref();
348
+ server.on("error", (err) => {
349
+ if (settled) return;
350
+ settled = true;
351
+ if (err.code === "EADDRINUSE") {
352
+ reject(
353
+ new Error(
354
+ `port ${port} is already in use \u2014 stop whatever is bound to it and try again`
355
+ )
356
+ );
357
+ return;
358
+ }
359
+ reject(err);
360
+ });
361
+ server.listen(port, () => {
362
+ if (!options.silent) {
363
+ console.log(`
364
+ Opening browser to sign in.`);
365
+ console.log(
366
+ `If it doesn't open, visit:
367
+ ${pc.cyan(authorizeUrl.toString())}
368
+ `
369
+ );
370
+ }
371
+ opener(authorizeUrl.toString());
372
+ });
373
+ });
374
+ }
375
+ async function postTokenRequest(authBaseUrl, body) {
376
+ const res = await fetch(`${authBaseUrl}/oauth2/token`, {
377
+ method: "POST",
378
+ headers: {
379
+ "Content-Type": "application/x-www-form-urlencoded",
380
+ Accept: "application/json"
381
+ },
382
+ body: body.toString()
383
+ });
384
+ const text2 = await res.text();
385
+ if (!res.ok) {
386
+ throw new Error(`token endpoint returned ${res.status}: ${text2}`);
387
+ }
388
+ let parsed;
389
+ try {
390
+ parsed = JSON.parse(text2);
391
+ } catch {
392
+ throw new Error(`token endpoint returned non-JSON: ${text2}`);
393
+ }
394
+ if (!parsed.access_token) {
395
+ throw new Error(`token endpoint response missing access_token: ${text2}`);
396
+ }
397
+ return parsed;
398
+ }
399
+ async function exchangeCodeForToken(authBaseUrl, clientId, code, verifier) {
400
+ return postTokenRequest(
401
+ authBaseUrl,
402
+ new URLSearchParams({
403
+ grant_type: "authorization_code",
404
+ code,
405
+ redirect_uri: REDIRECT_URI,
406
+ client_id: clientId,
407
+ code_verifier: verifier
408
+ })
409
+ );
410
+ }
411
+ async function refreshAccessToken(authBaseUrl, clientId, refreshToken) {
412
+ return postTokenRequest(
413
+ authBaseUrl,
414
+ new URLSearchParams({
415
+ grant_type: "refresh_token",
416
+ refresh_token: refreshToken,
417
+ client_id: clientId
418
+ })
419
+ );
420
+ }
421
+ async function acquireAccessToken(opts) {
422
+ const { auth, clientId, scope, reauth, spinner: spinner3, captureOptions } = opts;
423
+ const now = Math.floor(Date.now() / 1e3);
424
+ const cached = reauth ? null : getCachedToken(auth, clientId, scope);
425
+ if (cached && cached.expires_at > now + TOKEN_EXPIRY_MARGIN_S) {
426
+ spinner3?.start("Using cached session");
427
+ spinner3?.stop("Signed in (cached)");
428
+ return cached.access_token;
429
+ }
430
+ if (cached?.refresh_token) {
431
+ spinner3?.start("Refreshing session");
432
+ try {
433
+ const refreshed = await refreshAccessToken(
434
+ auth,
435
+ clientId,
436
+ cached.refresh_token
437
+ );
438
+ saveCachedToken(
439
+ auth,
440
+ makeCachedToken(clientId, refreshed, cached.refresh_token, scope)
441
+ );
442
+ spinner3?.stop("Signed in (refreshed)");
443
+ return refreshed.access_token;
444
+ } catch {
445
+ spinner3?.stop("Refresh failed, opening browser");
446
+ }
447
+ }
448
+ const pkce = generatePkce();
449
+ const state = base64url(crypto.randomBytes(16));
450
+ const authorizeUrl = new URL("/oauth2/auth", auth);
451
+ authorizeUrl.searchParams.set("response_type", "code");
452
+ authorizeUrl.searchParams.set("client_id", clientId);
453
+ authorizeUrl.searchParams.set("redirect_uri", REDIRECT_URI);
454
+ authorizeUrl.searchParams.set("scope", scope);
455
+ authorizeUrl.searchParams.set("state", state);
456
+ authorizeUrl.searchParams.set("code_challenge", pkce.challenge);
457
+ authorizeUrl.searchParams.set("code_challenge_method", "S256");
458
+ spinner3?.start("Waiting for browser login");
459
+ try {
460
+ const { code, verifier } = await captureAuthCode(
461
+ authorizeUrl,
462
+ state,
463
+ pkce.verifier,
464
+ captureOptions
465
+ );
466
+ spinner3?.message("Exchanging authorization code");
467
+ const tokens = await exchangeCodeForToken(auth, clientId, code, verifier);
468
+ saveCachedToken(auth, makeCachedToken(clientId, tokens, void 0, scope));
469
+ spinner3?.stop("Signed in");
470
+ return tokens.access_token;
471
+ } catch (err) {
472
+ spinner3?.stop("Login failed");
473
+ throw err;
474
+ }
475
+ }
476
+ async function apiCall(url, init) {
477
+ const res = await fetch(url, init);
478
+ const text2 = await res.text();
479
+ let body;
480
+ try {
481
+ body = text2 ? JSON.parse(text2) : {};
482
+ } catch {
483
+ body = text2;
484
+ }
485
+ if (!res.ok) {
486
+ const detail = typeof body === "string" ? body : JSON.stringify(body);
487
+ throw new Error(
488
+ `${init.method ?? "GET"} ${url} returned ${res.status}: ${detail}`
489
+ );
490
+ }
491
+ return body;
492
+ }
493
+ async function fetchTenants(apiBaseUrl, jwt) {
494
+ const tenants = await apiCall(`${apiBaseUrl}/account/tenants`, {
495
+ headers: { Authorization: `Bearer ${jwt}`, Accept: "application/json" }
496
+ });
497
+ if (!Array.isArray(tenants)) {
498
+ throw new Error(
499
+ `unexpected /account/tenants response: ${JSON.stringify(tenants)}`
500
+ );
501
+ }
502
+ return tenants;
503
+ }
504
+ async function listTenantApps(apiBaseUrl, jwt, tenantId) {
505
+ const res = await apiCall(
506
+ `${apiBaseUrl}/app-service/tenants/apps?onlyAppsOfTenant=true&returnAll=true`,
507
+ {
508
+ headers: {
509
+ Authorization: `Bearer ${jwt}`,
510
+ "X-Tenant-Id": tenantId,
511
+ Accept: "application/json"
512
+ }
513
+ }
514
+ );
515
+ const items = Array.isArray(res) ? res : res.items ?? [];
516
+ return items.map((entry) => {
517
+ const id = entry.appId ?? entry.app?.id;
518
+ const technicalName = entry.app?.technicalName;
519
+ if (!id || !technicalName) return null;
520
+ return {
521
+ id,
522
+ technicalName,
523
+ clientId: entry.app?.clientId,
524
+ provisioningStatus: entry.app?.activeVersion?.provisioningStatus
525
+ };
526
+ }).filter((a) => a !== null);
527
+ }
528
+ function validateHost(input, flag) {
529
+ let url;
530
+ try {
531
+ url = new URL(input);
532
+ } catch {
533
+ bail(`invalid --${flag} value: ${input}`);
534
+ }
535
+ if (!/^https?:$/.test(url.protocol)) {
536
+ bail(`--${flag} must use http(s): ${input}`);
537
+ }
538
+ return `${url.protocol}//${url.host}`;
539
+ }
540
+ function parseCollisionAction(input) {
541
+ if (input === "overwrite" || input === "print" || input === "cancel") {
542
+ return input;
543
+ }
544
+ bail(
545
+ `--on-collision must be one of: overwrite, print, cancel (got "${input}")`
546
+ );
547
+ }
548
+ function resolveOptions(args) {
549
+ let parsed;
550
+ try {
551
+ parsed = parseArgs({
552
+ args,
553
+ options: {
554
+ "api-host": { type: "string" },
555
+ "auth-host": { type: "string" },
556
+ "client-id": { type: "string" },
557
+ scope: { type: "string" },
558
+ reauth: { type: "boolean" },
559
+ yes: { type: "boolean", short: "y" },
560
+ tenant: { type: "string" },
561
+ "existing-app-id": { type: "string" },
562
+ new: { type: "boolean" },
563
+ "on-collision": { type: "string" }
564
+ },
565
+ strict: true,
566
+ allowPositionals: false
567
+ });
568
+ } catch (err) {
569
+ bail(err.message);
570
+ }
571
+ const api = parsed.values["api-host"] ? validateHost(parsed.values["api-host"], "api-host") : DEFAULT_API_HOST;
572
+ const auth = parsed.values["auth-host"] ? validateHost(parsed.values["auth-host"], "auth-host") : DEFAULT_AUTH_HOST;
573
+ const clientIdArg = parsed.values["client-id"];
574
+ const clientId = clientIdArg ?? CLIENT_IDS_BY_AUTH_HOST[new URL(auth).hostname] ?? "";
575
+ if (!clientId) {
576
+ bail(
577
+ `no OAuth client_id registered for ${auth}. Pass --client-id <id> or add an entry to CLIENT_IDS_BY_AUTH_HOST.`
578
+ );
579
+ }
580
+ const existingAppId = parsed.values["existing-app-id"];
581
+ const forceNew = parsed.values.new ?? false;
582
+ if (existingAppId && forceNew) {
583
+ bail("--existing-app-id and --new are mutually exclusive");
584
+ }
585
+ const onCollisionRaw = parsed.values["on-collision"];
586
+ const onCollision = onCollisionRaw ? parseCollisionAction(onCollisionRaw) : void 0;
587
+ return {
588
+ api,
589
+ auth,
590
+ clientId,
591
+ scope: parsed.values.scope ?? DEFAULT_SCOPE,
592
+ reauth: parsed.values.reauth ?? false,
593
+ yes: parsed.values.yes ?? false,
594
+ tenant: parsed.values.tenant,
595
+ existingAppId,
596
+ forceNew,
597
+ onCollision
598
+ };
599
+ }
600
+ function ensureContinue(value) {
601
+ if (p.isCancel(value)) {
602
+ bail("Cancelled.");
603
+ }
604
+ return value;
605
+ }
606
+ async function selectTenant(tenants, opts) {
607
+ if (tenants.length === 0) {
608
+ bail(
609
+ "your account has no tenants. Create one on the Partner Portal first."
610
+ );
611
+ }
612
+ if (opts.tenant) {
613
+ const needle = opts.tenant.toLowerCase();
614
+ const matches = tenants.filter(
615
+ (t) => t.id === opts.tenant || t.slug.toLowerCase() === needle
616
+ );
617
+ if (matches.length === 0) {
618
+ bail(`no tenant matched --tenant "${opts.tenant}"`);
619
+ }
620
+ if (matches.length > 1) {
621
+ bail(
622
+ `--tenant "${opts.tenant}" matched ${matches.length} tenants \u2014 be more specific`
623
+ );
624
+ }
625
+ return matches[0];
626
+ }
627
+ if (tenants.length === 1) {
628
+ return tenants[0];
629
+ }
630
+ if (opts.yes) {
631
+ bail(
632
+ `multiple tenants available; pass --tenant <id|slug> in non-interactive mode`
633
+ );
634
+ }
635
+ const tenantId = ensureContinue(
636
+ await p.select({
637
+ message: "Register the app in which tenant?",
638
+ options: tenants.map((t) => ({
639
+ value: t.id,
640
+ label: t.name,
641
+ hint: t.slug
642
+ }))
643
+ })
644
+ );
645
+ return tenants.find((t) => t.id === tenantId);
646
+ }
647
+ async function selectExistingOrNew(matches, opts) {
648
+ if (opts.existingAppId) {
649
+ const match = matches.find((a) => a.id === opts.existingAppId);
650
+ if (!match) {
651
+ bail(
652
+ `--existing-app-id "${opts.existingAppId}" not found among apps matching this manifest`
653
+ );
654
+ }
655
+ return match;
656
+ }
657
+ if (opts.forceNew) return null;
658
+ if (matches.length === 0) return null;
659
+ if (opts.yes) {
660
+ bail(
661
+ `${matches.length} existing app(s) match this manifest; pass --existing-app-id <id> or --new in non-interactive mode`
662
+ );
663
+ }
664
+ const CREATE_NEW = "__create_new__";
665
+ const choice = ensureContinue(
666
+ await p.select({
667
+ message: "Update an existing app or create a new one?",
668
+ options: [
669
+ ...matches.map((a) => ({
670
+ value: a.id,
671
+ label: `${a.technicalName} (${a.id})`,
672
+ hint: a.provisioningStatus
673
+ })),
674
+ { value: CREATE_NEW, label: "Create a new app" }
675
+ ]
676
+ })
677
+ );
678
+ if (choice === CREATE_NEW) return null;
679
+ return matches.find((a) => a.id === choice);
680
+ }
681
+ async function confirmAction(opts, message, defaultValue) {
682
+ if (opts.yes) return true;
683
+ return ensureContinue(
684
+ await p.confirm({ message, initialValue: defaultValue })
685
+ );
686
+ }
687
+ async function resolveCollision(targetLabel, credentialPair, opts) {
688
+ if (opts.onCollision) return opts.onCollision;
689
+ if (opts.yes) {
690
+ bail(
691
+ `${targetLabel} already has ${credentialPair}; pass --on-collision <overwrite|print|cancel> in non-interactive mode`
692
+ );
693
+ }
694
+ return ensureContinue(
695
+ await p.select({
696
+ message: `${targetLabel} already has ${credentialPair}. What now?`,
697
+ options: [
698
+ { value: "overwrite", label: "Overwrite the file" },
699
+ { value: "print", label: "Print to console, don't touch the file" },
700
+ { value: "cancel", label: "Cancel" }
701
+ ]
702
+ })
703
+ );
704
+ }
705
+ async function updateAppManifest(apiBaseUrl, jwt, tenantId, appId, manifest) {
706
+ await apiCall(`${apiBaseUrl}/app-service/store/apps/${appId}/manifest`, {
707
+ method: "PUT",
708
+ headers: {
709
+ Authorization: `Bearer ${jwt}`,
710
+ "Content-Type": "application/json",
711
+ "X-Tenant-Id": tenantId
712
+ },
713
+ body: JSON.stringify({ manifest: manifest.manifest, tcAccepted: true })
714
+ });
715
+ }
716
+ async function createDraftApp(apiBaseUrl, jwt, tenantId, manifest) {
717
+ const created = await apiCall(
718
+ `${apiBaseUrl}/app-service/store/apps`,
719
+ {
720
+ method: "POST",
721
+ headers: {
722
+ Authorization: `Bearer ${jwt}`,
723
+ "Content-Type": "application/json",
724
+ "X-Tenant-Id": tenantId
725
+ },
726
+ body: JSON.stringify({
727
+ manifest: manifest.manifest,
728
+ tcAccepted: true
729
+ })
730
+ }
731
+ );
732
+ if (!created.appId) {
733
+ throw new Error(
734
+ `/store/apps did not return appId: ${JSON.stringify(created)}`
735
+ );
736
+ }
737
+ return created.appId;
738
+ }
739
+ async function provisionApp(apiBaseUrl, jwt, tenantId, appId) {
740
+ const provisioned = await apiCall(
741
+ `${apiBaseUrl}/app-service/store/apps/${appId}/provision`,
742
+ {
743
+ method: "PUT",
744
+ headers: {
745
+ Authorization: `Bearer ${jwt}`,
746
+ "Content-Type": "application/json",
747
+ "X-Tenant-Id": tenantId
748
+ },
749
+ body: JSON.stringify({ appId })
750
+ }
751
+ );
752
+ const clientId = provisioned.app?.clientId;
753
+ const clientSecret = provisioned.clientSecret;
754
+ if (!clientId || !clientSecret) {
755
+ throw new Error(
756
+ `provision response missing app.clientId or clientSecret: ${JSON.stringify(provisioned)}`
757
+ );
758
+ }
759
+ return { clientId, clientSecret };
760
+ }
761
+ function credentialLabels(backend) {
762
+ if (backend.kind === "node") {
763
+ return {
764
+ id: "CLIENT_ID",
765
+ secret: "CLIENT_SECRET",
766
+ pair: "CLIENT_ID/CLIENT_SECRET"
767
+ };
768
+ }
769
+ return {
770
+ id: "JtlPlatform:ClientId",
771
+ secret: "JtlPlatform:ClientSecret",
772
+ pair: "JtlPlatform:ClientId/ClientSecret"
773
+ };
774
+ }
775
+ async function persistCredentials(backend, clientId, clientSecret, root, opts) {
776
+ const targetPath = backend.kind === "node" ? backend.envFile : backend.settingsFile;
777
+ const relPath = path.relative(root, targetPath) || targetPath;
778
+ const labels = credentialLabels(backend);
779
+ const existing = readExistingCredentials(backend);
780
+ const collision = !!(existing.clientId && existing.clientSecret);
781
+ let action = "overwrite";
782
+ if (collision) {
783
+ action = await resolveCollision(relPath, labels.pair, opts);
784
+ }
785
+ if (action === "cancel") bail("Cancelled.");
786
+ if (action === "print") {
787
+ console.log();
788
+ console.log(pc.cyan(labels.id) + " = " + clientId);
789
+ console.log(pc.cyan(labels.secret) + " = " + clientSecret);
790
+ console.log();
791
+ p.outro(pc.green("Done. Credentials printed above; file left untouched."));
792
+ return;
793
+ }
794
+ const written = writeBackendCredentials(backend, clientId, clientSecret);
795
+ p.outro(
796
+ pc.green(
797
+ `Done. Wrote ${labels.pair} to ${pc.cyan(path.relative(root, written) || written)}.`
798
+ )
799
+ );
800
+ }
801
+ async function register(argv = [], runOpts = {}) {
802
+ const root = runOpts.cwd ?? process.cwd();
803
+ const manifestPath = path.join(root, "manifest.json");
804
+ console.log();
805
+ p.intro(pc.bgCyan(pc.black(" register cloud app ")));
806
+ const opts = resolveOptions(argv);
807
+ const manifest = readManifest(manifestPath);
808
+ const summary = summarizeManifest(manifest);
809
+ const backend = detectBackend(root);
810
+ p.note(
811
+ [
812
+ `${pc.dim("API")} ${opts.api}`,
813
+ `${pc.dim("Auth")} ${opts.auth}`,
814
+ `${pc.dim("Client ID")} ${opts.clientId}`,
815
+ `${pc.dim("Scope")} ${opts.scope}`
816
+ ].join("\n"),
817
+ "Target"
818
+ );
819
+ const spinner3 = p.spinner();
820
+ let jwt;
821
+ try {
822
+ jwt = await acquireAccessToken({
823
+ auth: opts.auth,
824
+ clientId: opts.clientId,
825
+ scope: opts.scope,
826
+ reauth: opts.reauth,
827
+ spinner: spinner3,
828
+ captureOptions: runOpts.captureOptions
829
+ });
830
+ } catch (err) {
831
+ bail(err.message);
832
+ }
833
+ spinner3.start("Fetching tenants");
834
+ let tenants;
835
+ try {
836
+ tenants = await fetchTenants(opts.api, jwt);
837
+ spinner3.stop(
838
+ `Found ${tenants.length} tenant${tenants.length === 1 ? "" : "s"}`
839
+ );
840
+ } catch (err) {
841
+ spinner3.stop("Failed to fetch tenants");
842
+ bail(err.message);
843
+ }
844
+ const tenant = await selectTenant(tenants, opts);
845
+ spinner3.start("Looking for apps matching this manifest");
846
+ let matches;
847
+ try {
848
+ const apps = await listTenantApps(opts.api, jwt, tenant.id);
849
+ matches = apps.filter((a) => a.technicalName === summary.technicalName);
850
+ spinner3.stop(
851
+ `Found ${matches.length} app${matches.length === 1 ? "" : "s"} with technical name ${pc.cyan(summary.technicalName)}`
852
+ );
853
+ } catch (err) {
854
+ spinner3.stop("Failed to look up tenant apps");
855
+ bail(err.message);
856
+ }
857
+ const existing = await selectExistingOrNew(matches, opts);
858
+ const reviewLines = [
859
+ `${pc.dim("App name")} ${summary.name}`,
860
+ `${pc.dim("Technical name")} ${summary.technicalName}`,
861
+ `${pc.dim("Version")} ${summary.version}`,
862
+ summary.description ? `${pc.dim("Description")} ${summary.description}` : null,
863
+ `${pc.dim("Tenant")} ${tenant.name} (${tenant.slug})`,
864
+ existing ? `${pc.dim("Updating")} ${existing.technicalName} (${existing.id}) [${existing.provisioningStatus ?? "?"}]` : `${pc.dim("Action")} create new`
865
+ ].filter(Boolean);
866
+ p.note(reviewLines.join("\n"), "Review");
867
+ const confirmMessage = existing ? `Update app "${existing.technicalName}" in tenant "${tenant.name}"?` : `Create app "${summary.name}" in tenant "${tenant.name}"?`;
868
+ const confirm2 = await confirmAction(opts, confirmMessage, existing !== null);
869
+ if (!confirm2) bail("Cancelled.");
870
+ if (existing) {
871
+ spinner3.start("Updating manifest");
872
+ try {
873
+ await updateAppManifest(opts.api, jwt, tenant.id, existing.id, manifest);
874
+ spinner3.stop("Manifest updated");
875
+ } catch (err) {
876
+ spinner3.stop("Failed to update manifest");
877
+ bail(err.message);
878
+ }
879
+ if (existing.clientId) {
880
+ const labels = credentialLabels(backend);
881
+ const envFile = backend.kind === "node" ? backend.envFile : backend.settingsFile;
882
+ const localCreds = readExistingCredentials(backend);
883
+ const hasLocalCreds = !!(localCreds.clientId && localCreds.clientSecret);
884
+ if (!hasLocalCreds) {
885
+ p.outro(
886
+ pc.yellow(
887
+ `Manifest updated. Your backend config is missing ${labels.pair} \u2014 they can't be recovered. Re-create the app to get a fresh secret.`
888
+ )
889
+ );
890
+ return;
891
+ }
892
+ p.outro(
893
+ pc.green(
894
+ `Manifest updated. Existing ${labels.pair} in ${pc.cyan(path.relative(root, envFile))} kept as-is.`
895
+ )
896
+ );
897
+ } else {
898
+ p.outro(pc.green("Manifest updated."));
899
+ }
900
+ return;
901
+ }
902
+ spinner3.start("Creating draft app");
903
+ let appId;
904
+ try {
905
+ appId = await createDraftApp(opts.api, jwt, tenant.id, manifest);
906
+ spinner3.stop(`Created app ${pc.cyan(appId)}`);
907
+ } catch (err) {
908
+ spinner3.stop("Failed to create app");
909
+ bail(err.message);
910
+ }
911
+ spinner3.start("Provisioning app");
912
+ let clientId;
913
+ let clientSecret;
914
+ try {
915
+ ({ clientId, clientSecret } = await provisionApp(
916
+ opts.api,
917
+ jwt,
918
+ tenant.id,
919
+ appId
920
+ ));
921
+ spinner3.stop("Provisioned");
922
+ } catch (err) {
923
+ spinner3.stop("Failed to provision app");
924
+ bail(err.message);
925
+ }
926
+ await persistCredentials(backend, clientId, clientSecret, root, opts);
927
+ }
928
+
929
+ // src/scaffold.ts
930
+ import crypto2 from "crypto";
931
+ import fs2 from "fs";
932
+ import path2 from "path";
11
933
  function toPascalCase(kebab) {
12
934
  return kebab.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
13
935
  }
@@ -15,7 +937,7 @@ function slugify(input) {
15
937
  return input.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
16
938
  }
17
939
  function buildTechnicalName(appName) {
18
- const hash = crypto.randomBytes(3).toString("hex");
940
+ const hash = crypto2.randomBytes(3).toString("hex");
19
941
  return `${slugify(appName)}-${hash}`;
20
942
  }
21
943
  var SHORT_DESCRIPTION_MAX_LENGTH = 150;
@@ -53,9 +975,9 @@ function transformPackageJson(content, appName, templateType, replacements) {
53
975
  return result + "\n";
54
976
  }
55
977
  function copyDir(src, dest, replacements, templateType, appName) {
56
- fs.mkdirSync(dest, { recursive: true });
57
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
58
- const srcPath = path.join(src, entry.name);
978
+ fs2.mkdirSync(dest, { recursive: true });
979
+ for (const entry of fs2.readdirSync(src, { withFileTypes: true })) {
980
+ const srcPath = path2.join(src, entry.name);
59
981
  let destName = entry.name;
60
982
  if (destName.startsWith("_git")) {
61
983
  destName = `.${destName.slice(1)}`;
@@ -63,25 +985,25 @@ function copyDir(src, dest, replacements, templateType, appName) {
63
985
  for (const [key, value] of Object.entries(replacements)) {
64
986
  destName = destName.replaceAll(key, value);
65
987
  }
66
- const destPath = path.join(dest, destName);
988
+ const destPath = path2.join(dest, destName);
67
989
  if (entry.isDirectory()) {
68
990
  copyDir(srcPath, destPath, replacements);
69
991
  } else if (entry.name === "package.json" && templateType && appName) {
70
- const content = fs.readFileSync(srcPath, "utf-8");
71
- fs.writeFileSync(
992
+ const content = fs2.readFileSync(srcPath, "utf-8");
993
+ fs2.writeFileSync(
72
994
  destPath,
73
995
  transformPackageJson(content, appName, templateType, replacements)
74
996
  );
75
997
  } else {
76
- const ext = path.extname(entry.name).toLowerCase();
998
+ const ext = path2.extname(entry.name).toLowerCase();
77
999
  if (BINARY_EXTENSIONS.has(ext)) {
78
- fs.copyFileSync(srcPath, destPath);
1000
+ fs2.copyFileSync(srcPath, destPath);
79
1001
  } else {
80
- let content = fs.readFileSync(srcPath, "utf-8");
1002
+ let content = fs2.readFileSync(srcPath, "utf-8");
81
1003
  for (const [key, value] of Object.entries(replacements)) {
82
1004
  content = content.replaceAll(key, value);
83
1005
  }
84
- fs.writeFileSync(destPath, content);
1006
+ fs2.writeFileSync(destPath, content);
85
1007
  }
86
1008
  }
87
1009
  }
@@ -106,7 +1028,7 @@ async function scaffold({
106
1028
  shared,
107
1029
  targetDir: customTargetDir
108
1030
  }) {
109
- const targetDir = customTargetDir ?? path.resolve(process.cwd(), appName);
1031
+ const targetDir = customTargetDir ?? path2.resolve(process.cwd(), appName);
110
1032
  const pascalName = toPascalCase(appName);
111
1033
  const baseReplacements = {
112
1034
  "{{APP_NAME}}": appName,
@@ -115,12 +1037,12 @@ async function scaffold({
115
1037
  "{{APP_DESCRIPTION}}": description,
116
1038
  "{{APP_DESCRIPTION_SHORT}}": buildShortDescription(description)
117
1039
  };
118
- if (fs.existsSync(targetDir)) {
1040
+ if (fs2.existsSync(targetDir)) {
119
1041
  throw new Error(`Directory "${appName}" already exists`);
120
1042
  }
121
- fs.mkdirSync(targetDir, { recursive: true });
1043
+ fs2.mkdirSync(targetDir, { recursive: true });
122
1044
  copyDir(shared.dir, targetDir, baseReplacements);
123
- const frontendDest = path.join(targetDir, "packages", "frontend");
1045
+ const frontendDest = path2.join(targetDir, "packages", "frontend");
124
1046
  const frontendReplacements = buildReplacements(baseReplacements, frontend);
125
1047
  copyDir(
126
1048
  frontend.dir,
@@ -129,7 +1051,7 @@ async function scaffold({
129
1051
  frontend.meta.type,
130
1052
  appName
131
1053
  );
132
- const backendDest = path.join(targetDir, "packages", "backend");
1054
+ const backendDest = path2.join(targetDir, "packages", "backend");
133
1055
  const backendReplacements = buildReplacements(baseReplacements, backend);
134
1056
  copyDir(
135
1057
  backend.dir,
@@ -141,8 +1063,8 @@ async function scaffold({
141
1063
  const runtimeVersions = backend.meta.runtimeVersions ?? {};
142
1064
  if (Object.keys(runtimeVersions).length > 0) {
143
1065
  const toolVersions = Object.entries(runtimeVersions).map(([tool, version]) => `${tool} ${version}`).join("\n");
144
- fs.writeFileSync(
145
- path.join(targetDir, ".tool-versions"),
1066
+ fs2.writeFileSync(
1067
+ path2.join(targetDir, ".tool-versions"),
146
1068
  toolVersions + "\n"
147
1069
  );
148
1070
  }
@@ -155,39 +1077,40 @@ async function scaffold({
155
1077
  dev: "turbo dev",
156
1078
  build: "turbo build",
157
1079
  lint: "turbo lint",
158
- test: "turbo test"
1080
+ test: "turbo test",
1081
+ register: "npx -y @jtl-software/create-cloud-app@latest register"
159
1082
  },
160
1083
  packageManager: "npm@10.8.2",
161
1084
  devDependencies: {
162
1085
  turbo: "^2.5.5"
163
1086
  }
164
1087
  };
165
- fs.writeFileSync(
166
- path.join(targetDir, "package.json"),
1088
+ fs2.writeFileSync(
1089
+ path2.join(targetDir, "package.json"),
167
1090
  JSON.stringify(rootPackageJson, null, 2) + "\n"
168
1091
  );
169
1092
  }
170
1093
 
171
1094
  // src/templates.ts
172
- import fs2 from "fs";
173
- import path2 from "path";
1095
+ import fs3 from "fs";
1096
+ import path3 from "path";
174
1097
  import { createRequire } from "module";
175
1098
  var require2 = createRequire(import.meta.url);
176
1099
  function discoverTemplates() {
177
1100
  const cliPkgPath = require2.resolve("@jtl-software/create-cloud-app/package.json");
178
- const cliPkg = JSON.parse(fs2.readFileSync(cliPkgPath, "utf-8"));
1101
+ const cliPkg = JSON.parse(fs3.readFileSync(cliPkgPath, "utf-8"));
179
1102
  const deps = cliPkg.dependencies ?? {};
180
1103
  const templates = [];
181
1104
  for (const packageName of Object.keys(deps)) {
182
1105
  if (!packageName.startsWith("@jtl-software/cloud-app-template-")) continue;
183
1106
  try {
184
1107
  const pkgJsonPath = require2.resolve(`${packageName}/package.json`);
185
- const pkg = JSON.parse(fs2.readFileSync(pkgJsonPath, "utf-8"));
1108
+ const pkg = JSON.parse(fs3.readFileSync(pkgJsonPath, "utf-8"));
186
1109
  const meta = pkg.cloudAppTemplate;
187
1110
  if (!meta?.type) continue;
188
1111
  templates.push({
189
1112
  packageName,
190
- dir: path2.dirname(pkgJsonPath),
1113
+ dir: path3.dirname(pkgJsonPath),
191
1114
  meta
192
1115
  });
193
1116
  } catch {
@@ -200,20 +1123,20 @@ function getTemplatesByType(templates, type) {
200
1123
  }
201
1124
 
202
1125
  // src/index.ts
203
- async function main() {
1126
+ async function runScaffold() {
204
1127
  console.log();
205
- p.intro(pc.bgCyan(pc.black(" create-cloud-app ")));
1128
+ p2.intro(pc2.bgCyan(pc2.black(" create-cloud-app ")));
206
1129
  const templates = discoverTemplates();
207
1130
  const backends = getTemplatesByType(templates, "backend");
208
1131
  const frontends = getTemplatesByType(templates, "frontend");
209
1132
  const shared = templates.find((t) => t.meta.type === "shared");
210
1133
  if (!backends.length || !frontends.length || !shared) {
211
- p.cancel("No templates found. Ensure template packages are installed.");
1134
+ p2.cancel("No templates found. Ensure template packages are installed.");
212
1135
  process.exit(1);
213
1136
  }
214
- const answers = await p.group(
1137
+ const answers = await p2.group(
215
1138
  {
216
- appName: () => p.text({
1139
+ appName: () => p2.text({
217
1140
  message: "App name",
218
1141
  placeholder: "my-jtl-app",
219
1142
  validate: (value) => {
@@ -222,11 +1145,11 @@ async function main() {
222
1145
  return "Use lowercase letters, numbers, and hyphens only";
223
1146
  }
224
1147
  }),
225
- description: () => p.text({
1148
+ description: () => p2.text({
226
1149
  message: "Description",
227
1150
  placeholder: "A JTL Platform app"
228
1151
  }),
229
- backend: () => p.select({
1152
+ backend: () => p2.select({
230
1153
  message: "Backend",
231
1154
  options: backends.map((t) => ({
232
1155
  value: t.packageName,
@@ -234,7 +1157,7 @@ async function main() {
234
1157
  hint: t.meta.hint
235
1158
  }))
236
1159
  }),
237
- frontend: () => p.select({
1160
+ frontend: () => p2.select({
238
1161
  message: "Frontend",
239
1162
  options: frontends.map((t) => ({
240
1163
  value: t.packageName,
@@ -245,7 +1168,7 @@ async function main() {
245
1168
  },
246
1169
  {
247
1170
  onCancel: () => {
248
- p.cancel("Cancelled.");
1171
+ p2.cancel("Cancelled.");
249
1172
  process.exit(0);
250
1173
  }
251
1174
  }
@@ -256,7 +1179,7 @@ async function main() {
256
1179
  const selectedFrontend = templates.find(
257
1180
  (t) => t.packageName === answers.frontend
258
1181
  );
259
- const s = p.spinner();
1182
+ const s = p2.spinner();
260
1183
  s.start("Scaffolding project");
261
1184
  await scaffold({
262
1185
  appName: answers.appName,
@@ -266,16 +1189,28 @@ async function main() {
266
1189
  shared
267
1190
  });
268
1191
  s.stop("Project scaffolded");
269
- p.note(
1192
+ p2.note(
270
1193
  [
271
- `${pc.cyan("cd")} ${answers.appName}`,
272
- `${pc.cyan("npm install")}`,
273
- `${pc.cyan("npm run dev")}`,
274
- "",
275
- pc.dim("Then register your manifest in the Partner Portal.")
1194
+ `${pc2.cyan("cd")} ${answers.appName}`,
1195
+ `${pc2.cyan("npm install")}`,
1196
+ `${pc2.cyan("npm run register")}`,
1197
+ `${pc2.cyan("npm run dev")}`
276
1198
  ].join("\n"),
277
1199
  "Next steps"
278
1200
  );
279
- p.outro(pc.green("Done! Happy building."));
1201
+ p2.outro(pc2.green("Done! Happy building."));
1202
+ }
1203
+ async function main() {
1204
+ if (process.argv[2] === "register") {
1205
+ await register(process.argv.slice(3));
1206
+ return;
1207
+ }
1208
+ await runScaffold();
280
1209
  }
281
- main().catch(console.error);
1210
+ main().catch((err) => {
1211
+ if (err instanceof BailError) {
1212
+ process.exit(1);
1213
+ }
1214
+ console.error(err);
1215
+ process.exit(1);
1216
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jtl-software/create-cloud-app",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "type": "module",
5
5
  "description": "CLI tool for scaffolding JTL Platform cloud apps",
6
6
  "bin": {
@@ -15,14 +15,15 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@clack/prompts": "^1.2.0",
18
- "@jtl-software/cloud-app-template-frontend-react": "0.0.12",
19
- "@jtl-software/cloud-app-template-backend-node": "0.0.9",
20
- "@jtl-software/cloud-app-template-backend-dotnet": "0.0.7",
21
- "@jtl-software/cloud-app-template-shared": "0.0.3",
18
+ "@jtl-software/cloud-app-template-backend-dotnet": "0.0.9",
19
+ "@jtl-software/cloud-app-template-backend-node": "0.0.11",
20
+ "@jtl-software/cloud-app-template-frontend-react": "0.0.13",
21
+ "@jtl-software/cloud-app-template-shared": "0.0.4",
22
22
  "picocolors": "^1.1.1"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^22.14.0",
26
+ "msw": "^2.14.6",
26
27
  "tsup": "^8.4.0",
27
28
  "tsx": "^4.19.3",
28
29
  "typescript": "^6.0.2"