@raisenow/tamaro-cli 1.8.0-dev.7 → 1.9.0-dev.1

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 CHANGED
@@ -185,7 +185,9 @@ Deploy a pre-built _Tamaro Core_ or customer configuration bundle to AWS S3.
185
185
 
186
186
  ### Options
187
187
 
188
+ - `--dryrun` – Displays the operations that would be performed without actually running them
188
189
  - `--tag <tag>` – Tag which should be used for the deployment of the bundle (default: "latest")
190
+ - `--skip-epms-sync-only-use-in-emergencies-or-you-will-be-fired` – Skip the EPMS sync entirely. Only use this in emergencies.
189
191
 
190
192
  Make sure you have built the bundle before deploying it with this command, otherwise you may mistakenly deploy the
191
193
  bundle from a previous build.
@@ -287,6 +289,15 @@ cd /path/to/tamaro-configurations/configs/example-02-typical-customisations
287
289
  npx -y @raisenow/tamaro-cli undeploy-email-config
288
290
  ```
289
291
 
292
+ ## `update-epms`
293
+
294
+ Sync the Tamaro configuration with EPMS (without deploying it).
295
+
296
+ ## Options
297
+
298
+ - `--dryrun` – Displays the operations that would be performed without actually running them
299
+ - `--tag <tag>` – Tag which should be used for the deployment of the bundle (default: "latest")
300
+
290
301
  ## `archive`
291
302
 
292
303
  Archive a customer configuration. This moves the configuration folder into the `_archived` folder.
package/dist/cli.js CHANGED
@@ -1,15 +1,23 @@
1
1
  #!/usr/bin/env node
2
- import { C as promptConfirmation, D as logError, E as logDataTable, O as logSuccess, S as halt, T as logCommand, _ as getWidgetUuid, a as AWS_S3_BUCKET_TAMARO, c as CORE_CONFIG_NAME, d as HTTPS_CRT_FILE, f as HTTPS_KEY_FILE, h as getPaths, i as AWS_CLOUDFRONT_DISTRIBUTION_ID, k as logTitle, l as DEFAULT_PORT, m as getIfCoreFns, n as assertEnvValid, o as AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_PROD, s as AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_STAGE, t as applyEnv, u as DEFAULT_TAG, w as runCommandSync, x as resolveOwn, y as resolveBin } from "./env-CSg7BrvF.js";
2
+ import { A as EPMS_AUTH_CLIENT_ID_STAGE, B as logSuccess, C as DEFAULT_PORT, D as EPMS_AUTH_BASE_URL_PROD, E as EPMS_API_BASE_URL_STAGE, F as runCommandSync, I as createTerminalLink, L as logCommand, M as HTTPS_KEY_FILE, N as halt, O as EPMS_AUTH_BASE_URL_STAGE, P as promptConfirmation, R as logDataTable, S as DEFAULT_HTTP_TIMEOUT, T as EPMS_API_BASE_URL_PROD, V as logTitle, _ as AWS_S3_BUCKET_TAMARO, a as getIfCoreFns, b as CACHE_DIR, c as getWidgetUuid, d as resolveBin, f as resolveCoreVersion, g as AWS_CLOUDFRONT_DISTRIBUTION_ID, h as AUTH_PORT, j as HTTPS_CRT_FILE, k as EPMS_AUTH_CLIENT_ID_PROD, l as resolveAccountUuidFromConfig, m as resolveOwn, n as assertEnvValid, o as getPaths, t as applyEnv, v as AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_PROD, w as DEFAULT_TAG, x as CORE_CONFIG_NAME, y as AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_STAGE, z as logError } from "./env-JzPIlctP.js";
3
3
  import { createCommand } from "commander";
4
- import fs, { existsSync, mkdirSync, renameSync } from "node:fs";
4
+ import fs, { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
5
5
  import path from "node:path";
6
+ import { S3 } from "@aws-sdk/client-s3";
6
7
  import { execaCommandSync } from "execa";
7
8
  import prompts from "prompts";
8
9
  import stripIndent from "strip-indent";
10
+ import columnify from "columnify";
9
11
  import notifier from "node-notifier";
12
+ import { z } from "zod";
10
13
  import { globSync } from "glob";
11
14
  import Handlebars from "handlebars";
12
15
  import helpers from "handlebars-helpers";
16
+ import { config } from "dotenv";
17
+ import * as yaml from "js-yaml";
18
+ import ky from "ky";
19
+ import { createServer } from "node:http";
20
+ import open from "open";
13
21
  import { getPortPromise } from "portfinder";
14
22
  //#region src/lib/aws.ts
15
23
  const awsAuthenticate = (options) => {
@@ -83,6 +91,28 @@ const promptProfile = async () => {
83
91
  }], { onCancel: () => halt() });
84
92
  return profile;
85
93
  };
94
+ const s3 = new S3({});
95
+ const getConfigDeployments = async (configName) => {
96
+ const widgetJs = (await s3.listObjectsV2({
97
+ Bucket: "tamaro.raisenow.com",
98
+ Prefix: `${configName}/`
99
+ })).Contents?.filter((file) => file.Key?.endsWith(`/index.html`)) ?? [];
100
+ const deployments = [];
101
+ for (const file of widgetJs) {
102
+ const key = file.Key ?? "";
103
+ const parts = key.split("/");
104
+ if (parts.length < 2 || !file.LastModified) continue;
105
+ const tag = parts.at(-2);
106
+ const date = file.LastModified.toISOString();
107
+ const fullUrl = `https://${AWS_S3_BUCKET_TAMARO}/${key}`;
108
+ deployments.push({
109
+ date,
110
+ fullUrl,
111
+ tag
112
+ });
113
+ }
114
+ return deployments;
115
+ };
86
116
  //#endregion
87
117
  //#region src/lib/notifier.ts
88
118
  const notify = (args) => {
@@ -98,7 +128,7 @@ const notify = (args) => {
98
128
  //#region src/commands/archive.ts
99
129
  const archive = (options) => {
100
130
  const { ifCore } = getIfCoreFns();
101
- assertIsNotCore$3(ifCore);
131
+ assertIsNotCore$4(ifCore);
102
132
  assertIsNotArchived();
103
133
  const configName = getWidgetUuid();
104
134
  const cwd = process.cwd();
@@ -124,7 +154,7 @@ const assertOptionsValid$9 = (options) => {
124
154
  const { profile } = options;
125
155
  if (profile) assertProfileValid(profile);
126
156
  };
127
- const assertIsNotCore$3 = (ifCore) => {
157
+ const assertIsNotCore$4 = (ifCore) => {
128
158
  if (ifCore()) halt("You cannot archive Tamaro Core.");
129
159
  };
130
160
  const assertIsNotArchived = () => {
@@ -324,6 +354,311 @@ const assertTagValid = (tag) => {
324
354
  if (!regex.test(tag)) halt(`Flag "--tag" has forbidden format. Allowed format: ${regex.toString()}.`);
325
355
  };
326
356
  //#endregion
357
+ //#region src/lib/epms/auth/util.ts
358
+ const CachedTokenSchema = z.object({
359
+ expirationTime: z.number(),
360
+ token: z.string()
361
+ });
362
+ const AuthResponseSchema = z.object({
363
+ access_token: z.string(),
364
+ expires_in: z.number(),
365
+ token_type: z.string()
366
+ });
367
+ const getCacheFilePath = (baseUrl, clientId) => {
368
+ const cacheKey = Buffer.from(`${baseUrl}|${clientId}`).toString("base64url");
369
+ return path.join(CACHE_DIR, `token-${cacheKey}.json`);
370
+ };
371
+ const readCachedToken = (baseUrl, clientId) => {
372
+ try {
373
+ const filePath = getCacheFilePath(baseUrl, clientId);
374
+ if (!existsSync(filePath)) return;
375
+ const data = JSON.parse(readFileSync(filePath, "utf8"));
376
+ const parsed = CachedTokenSchema.safeParse(data);
377
+ if (parsed.success) return parsed.data;
378
+ } catch {}
379
+ };
380
+ const writeCachedToken = (baseUrl, clientId, cached) => {
381
+ try {
382
+ const filePath = getCacheFilePath(baseUrl, clientId);
383
+ mkdirSync(path.dirname(filePath), { recursive: true });
384
+ writeFileSync(filePath, JSON.stringify(cached), { mode: 384 });
385
+ } catch {}
386
+ };
387
+ const removeCachedToken = (baseUrl, clientId) => {
388
+ try {
389
+ const filePath = getCacheFilePath(baseUrl, clientId);
390
+ if (existsSync(filePath)) writeFileSync(filePath, "", { flag: "w" });
391
+ } catch {}
392
+ };
393
+ //#endregion
394
+ //#region src/lib/epms/auth/apiClient.ts
395
+ const createAuthorizer = (baseUrl, clientId, clientSecret) => {
396
+ const apiUnauthorized = ky.extend({
397
+ headers: { "Access-Control-Allow-Origin": "*" },
398
+ prefixUrl: baseUrl,
399
+ timeout: DEFAULT_HTTP_TIMEOUT
400
+ });
401
+ let expirationTime = void 0;
402
+ let token = void 0;
403
+ const cached = readCachedToken(baseUrl, clientId);
404
+ if (cached) {
405
+ token = cached.token;
406
+ expirationTime = cached.expirationTime;
407
+ }
408
+ const authorize = async () => {
409
+ const isExpired = (expirationTime ?? 0) - Date.now() < 300 * 1e3;
410
+ if (token && !isExpired) return token;
411
+ const data = await apiUnauthorized.post("oauth2/token", { json: {
412
+ client_id: clientId,
413
+ client_secret: clientSecret,
414
+ grant_type: "client_credentials"
415
+ } }).json();
416
+ token = `${data.token_type} ${data.access_token}`;
417
+ expirationTime = Date.now() + data.expires_in * 1e3;
418
+ writeCachedToken(baseUrl, clientId, {
419
+ expirationTime,
420
+ token
421
+ });
422
+ return token;
423
+ };
424
+ return apiUnauthorized.extend({ hooks: { beforeRequest: [async (request) => {
425
+ const authToken = await authorize();
426
+ request.headers.set("Authorization", authToken);
427
+ }] } });
428
+ };
429
+ //#endregion
430
+ //#region src/lib/epms/auth/authCallback.html
431
+ var authCallback_default = "<!doctype html>\n<html>\n <head\n ><title>Authenticating</title></head\n >\n <body>\n <p>Authenticating</p>\n <script>\n const hash = window.location.hash.substring(1)\n const params = new URLSearchParams(hash)\n const data = {\n access_token: params.get('access_token'),\n token_type: params.get('token_type'),\n expires_in: parseInt(params.get('expires_in') || '0', 10),\n }\n fetch('/token', {\n method: 'POST',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify(data),\n })\n .then(() => {\n document.querySelector('p').textContent =\n 'Authentication successful! You can close this tab.'\n })\n .catch(() => {\n document.querySelector('p').textContent =\n 'Authentication failed. Please try again.'\n })\n <\/script>\n </body>\n</html>\n";
432
+ //#endregion
433
+ //#region src/lib/epms/auth/browser.ts
434
+ /**
435
+ * The following authentication method is a browser-based OAuth flow.
436
+ * A very similar flow is used by tools like AWS CLI when running `aws sso login` or Claude Code.
437
+ *
438
+ * The flow works as follows:
439
+ * 1. The CLI starts a temporary local HTTP server that listens for the OAuth callback.
440
+ * 2. The CLI opens the user's default web browser and navigates to the EPMS authorization URL, passing the local server's callback URL as the redirect_uri.
441
+ * 3. The user authenticates in the browser, and the OAuth server redirects back to the local server with the authorization token.
442
+ * 4. The local server captures the token and resolves the promise, completing the authentication flow.
443
+ *
444
+ *
445
+ * We also implement a simple caching mechanism that stores the token in a file in the user's home directory.
446
+ * This allows us to reuse the token for subsequent requests until it expires, at which point we automatically trigger a new authentication flow.
447
+ */
448
+ const doBrowserOAuthFlow = (authBaseUrl, clientId, port) => {
449
+ return new Promise((resolve, reject) => {
450
+ const server = createServer((req, res) => {
451
+ if (req.method === "POST" && req.url === "/token") {
452
+ let body = "";
453
+ req.on("data", (chunk) => {
454
+ body += chunk.toString();
455
+ });
456
+ req.on("end", () => {
457
+ try {
458
+ const data = AuthResponseSchema.parse(JSON.parse(body));
459
+ res.writeHead(200);
460
+ res.end();
461
+ server.close();
462
+ resolve(data);
463
+ } catch (error) {
464
+ res.writeHead(400);
465
+ res.end();
466
+ reject(error instanceof Error ? error : new Error(String(error)));
467
+ }
468
+ });
469
+ } else {
470
+ res.writeHead(200, { "Content-Type": "text/html" });
471
+ res.end(authCallback_default);
472
+ }
473
+ });
474
+ server.on("error", reject);
475
+ server.listen(port, () => {
476
+ const redirectUri = `http://localhost:${port}`;
477
+ const authUrl = `${authBaseUrl}/oauth2/authorize?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=token`;
478
+ console.log(`Opening browser for authentication: ${authUrl}`);
479
+ open(authUrl).catch((error) => {
480
+ reject(error instanceof Error ? error : new Error(String(error)));
481
+ });
482
+ });
483
+ });
484
+ };
485
+ const createBrowserAuthorizer = (authBaseUrl, clientId, epmsBaseUrl) => {
486
+ const apiBase = ky.extend({
487
+ prefixUrl: epmsBaseUrl,
488
+ timeout: DEFAULT_HTTP_TIMEOUT
489
+ });
490
+ let expirationTime = void 0;
491
+ let token = void 0;
492
+ const cacheBaseUrl = `browser|${authBaseUrl}`;
493
+ const cached = readCachedToken(cacheBaseUrl, clientId);
494
+ if (cached) {
495
+ token = cached.token;
496
+ expirationTime = cached.expirationTime;
497
+ }
498
+ const resetAuth = () => {
499
+ token = void 0;
500
+ expirationTime = void 0;
501
+ removeCachedToken(cacheBaseUrl, clientId);
502
+ };
503
+ const authorize = async () => {
504
+ const isExpired = (expirationTime ?? 0) - Date.now() < 300 * 1e3;
505
+ if (token && !isExpired) return token;
506
+ const data = await doBrowserOAuthFlow(authBaseUrl, clientId, AUTH_PORT);
507
+ token = `${data.token_type} ${data.access_token}`;
508
+ expirationTime = Date.now() + data.expires_in * 1e3;
509
+ writeCachedToken(cacheBaseUrl, clientId, {
510
+ expirationTime,
511
+ token
512
+ });
513
+ return token;
514
+ };
515
+ return apiBase.extend({ hooks: {
516
+ afterResponse: [async (request, options, response) => {
517
+ if (response.status === 401) {
518
+ resetAuth();
519
+ const newToken = await authorize();
520
+ const newRequest = new Request(request, { headers: new Headers(request.headers) });
521
+ newRequest.headers.set("Authorization", newToken);
522
+ return ky(newRequest, options);
523
+ }
524
+ return response;
525
+ }],
526
+ beforeRequest: [async (request) => {
527
+ const authToken = await authorize();
528
+ request.headers.set("Authorization", authToken);
529
+ }]
530
+ } });
531
+ };
532
+ //#endregion
533
+ //#region src/lib/epms/client.ts
534
+ var EpmsClient = class EpmsClient {
535
+ #epmsClient;
536
+ constructor(epmsClient) {
537
+ this.#epmsClient = epmsClient;
538
+ }
539
+ static fromClientCredentials(baseUrl, clientId, clientSecret) {
540
+ return new EpmsClient(createAuthorizer(baseUrl, clientId, clientSecret));
541
+ }
542
+ static fromBrowserAuth(epmsBaseUrl, authBaseUrl, clientId) {
543
+ return new EpmsClient(createBrowserAuthorizer(authBaseUrl, clientId, epmsBaseUrl));
544
+ }
545
+ async getOrganisationIdByAccountUuid(accountUuid) {
546
+ const query = {
547
+ from: 0,
548
+ query: { $and: [{ $term: { object_uuid: accountUuid } }, { $term: { object: "account" } }] },
549
+ size: 1
550
+ };
551
+ const response = await this.#epmsClient.post("search/events", { json: query }).json();
552
+ if (response.hits.length === 0) throw new Error(`No events found for account UUID: ${accountUuid}`);
553
+ const organisationId = response.hits[0].organisation_uuid;
554
+ if (!organisationId) throw new Error(`Organisation UUID not found for account UUID: ${accountUuid}`);
555
+ return organisationId;
556
+ }
557
+ async updateTamaro(configName, tag, json) {
558
+ return this.#epmsClient.put(`products/tamaro/${configName}/tags/${tag}`, { json }).json();
559
+ }
560
+ };
561
+ //#endregion
562
+ //#region src/commands/update-epms.ts
563
+ const updateEpms = async (options) => {
564
+ const { ifCore } = getIfCoreFns();
565
+ assertIsNotCore$3(ifCore);
566
+ if (!options.profile && !options.ci) {
567
+ assertProfilesPresent();
568
+ options.profile = await promptProfile();
569
+ }
570
+ if (options.profile) {
571
+ assertProfileValid(options.profile);
572
+ process.env.AWS_PROFILE = options.profile;
573
+ }
574
+ awsAuthenticate(options);
575
+ const paths = getPaths(ifCore);
576
+ const { dryrun, tag } = options;
577
+ const configName = getWidgetUuid();
578
+ loadEnv(paths);
579
+ const { accountUuidsProd, accountUuidsStage } = loadAccountUuidsFromConfig(paths);
580
+ if (dryrun) logTitle("🧪 DRY RUN MODE - No actual changes will be made 🧪");
581
+ const configuredVersion = resolveCoreVersion();
582
+ if (!configuredVersion) {
583
+ logTitle("⚠️ Could not determine the configured Tamaro version. Please ensure that the CORE_VERSION environment variable is set or that the CORE_URL environment variable contains a valid Tamaro version.");
584
+ logTitle("This may be expected behaviour if you are using a custom build of Tamaro Core. If so, you can safely ignore this warning. ⚠️");
585
+ }
586
+ const baseInfo = {
587
+ configured_version: configuredVersion ?? "unknown",
588
+ last_deployment: Math.trunc(Date.now() / 1e3),
589
+ read_only: false
590
+ };
591
+ for (const accountUuidStage of accountUuidsStage) if (accountUuidStage) {
592
+ const epmsClient = epmsAuthenticate("stage");
593
+ const organisationUuid = await epmsClient.getOrganisationIdByAccountUuid(accountUuidStage);
594
+ const body = {
595
+ ...baseInfo,
596
+ account_uuid: accountUuidStage,
597
+ organisation_uuid: organisationUuid
598
+ };
599
+ logTitle(`Updating EPMS Stage for tag "${tag}":`);
600
+ logDataTable({
601
+ name: configName,
602
+ tag,
603
+ ...body
604
+ });
605
+ if (!dryrun) {
606
+ await epmsClient.updateTamaro(configName, tag, body);
607
+ logSuccess("✅ EPMS Stage is updated.");
608
+ } else logSuccess("⏭️ EPMS Stage would be updated (dry run).");
609
+ }
610
+ for (const accountUuidProd of accountUuidsProd) if (accountUuidProd) {
611
+ const epmsClient = epmsAuthenticate("prod");
612
+ const organisationUuid = await epmsClient.getOrganisationIdByAccountUuid(accountUuidProd);
613
+ logTitle(`Updating EPMS Prod for tag "${tag}":`);
614
+ const body = {
615
+ ...baseInfo,
616
+ account_uuid: accountUuidProd,
617
+ organisation_uuid: organisationUuid
618
+ };
619
+ logDataTable({
620
+ name: configName,
621
+ tag,
622
+ ...body
623
+ });
624
+ if (!dryrun) {
625
+ await epmsClient.updateTamaro(configName, tag, body);
626
+ logSuccess("✅ EPMS Prod is updated.");
627
+ } else logSuccess("⏭️ EPMS Prod would be updated (dry run).");
628
+ }
629
+ };
630
+ const epmsAuthenticate = (env) => {
631
+ if (env === "stage") {
632
+ if (process.env.EPMS_CLIENT_ID_STAGE && process.env.EPMS_CLIENT_SECRET_STAGE) return EpmsClient.fromClientCredentials(EPMS_API_BASE_URL_STAGE, process.env.EPMS_CLIENT_ID_STAGE, process.env.EPMS_CLIENT_SECRET_STAGE);
633
+ return EpmsClient.fromBrowserAuth(EPMS_API_BASE_URL_STAGE, EPMS_AUTH_BASE_URL_STAGE, EPMS_AUTH_CLIENT_ID_STAGE);
634
+ }
635
+ if (process.env.EPMS_CLIENT_ID && process.env.EPMS_CLIENT_SECRET) return EpmsClient.fromClientCredentials(EPMS_API_BASE_URL_PROD, process.env.EPMS_CLIENT_ID, process.env.EPMS_CLIENT_SECRET);
636
+ return EpmsClient.fromBrowserAuth(EPMS_API_BASE_URL_PROD, EPMS_AUTH_BASE_URL_PROD, EPMS_AUTH_CLIENT_ID_PROD);
637
+ };
638
+ const loadAccountUuidsFromConfig = (paths) => {
639
+ if (!("configYml" in paths)) {
640
+ halt("Fatal error, this should never happen");
641
+ throw new Error("Fatal error");
642
+ }
643
+ const configContent = yaml.load(readFileSync(paths.configYml, "utf8"));
644
+ const accountUuidsStage = resolveAccountUuidFromConfig(configContent, "epms_stage");
645
+ return {
646
+ accountUuidsProd: resolveAccountUuidFromConfig(configContent, "epms"),
647
+ accountUuidsStage
648
+ };
649
+ };
650
+ const loadEnv = (paths) => {
651
+ const filePath = globSync(paths.appEnv).find((file) => path.basename(file) === ".env");
652
+ if (!filePath) halt("No \".env\" file found. Are you in the folder of the Tamaro configuration?");
653
+ config({
654
+ path: filePath,
655
+ quiet: true
656
+ });
657
+ };
658
+ const assertIsNotCore$3 = (ifCore) => {
659
+ if (ifCore()) halt("You cannot update EPMS in the context of Tamaro Core.");
660
+ };
661
+ //#endregion
327
662
  //#region src/commands/deploy.ts
328
663
  const deploy = async (options) => {
329
664
  const { ifCore } = getIfCoreFns();
@@ -395,6 +730,8 @@ const deploy = async (options) => {
395
730
  halt(error.stderr);
396
731
  }
397
732
  }
733
+ if (options.skipEpmsSyncOnlyUseInEmergenciesOrYouWillBeFired) logTitle("⚠️ EPMS sync skipped. You better know what you are doing.");
734
+ else await updateEpms(options);
398
735
  const demoPage = `https://${AWS_S3_BUCKET_TAMARO}/${configName}/${tag}/index.html`;
399
736
  const entryPoint = `https://${AWS_S3_BUCKET_TAMARO}/${configName}/${tag}/${entryFilename}`;
400
737
  logTitle(`\nBundle for "${configName}" is deployed with tag "${tag}".`);
@@ -543,26 +880,32 @@ const listDeployed = async (options) => {
543
880
  }
544
881
  assertOptionsValid$4(options);
545
882
  awsAuthenticate(options);
546
- const flags = prepareFlags$3(options);
547
- const { config } = options;
883
+ const { config, profile } = options;
548
884
  const { ifCore } = getIfCoreFns();
549
885
  const configName = config ?? ifCore("tamaro-core", getWidgetUuid());
550
- const title = !config && ifCore() ? `Listing deployments of Tamaro Core …` : `Listing deployments of "${configName}" customer configuration …`;
551
- const cmd = `aws s3 ls ${`s3://${AWS_S3_BUCKET_TAMARO}/${configName}/`} ${flags}`;
552
- let out = "";
553
- logTitle(title);
554
- logCommand(cmd);
555
- try {
556
- out = runCommandSync(cmd).stdout;
557
- } catch (error) {
558
- out = error.stdout;
559
- if (error.stderr) halt(error.stderr);
886
+ logTitle(!config && ifCore() ? `Listing deployments of Tamaro Core …` : `Listing deployments of "${configName}" customer configuration …`);
887
+ if (profile) process.env.AWS_PROFILE = profile;
888
+ const deployments = await getConfigDeployments(configName);
889
+ if (deployments.length === 0) {
890
+ console.log("\nNo deployments found.\n");
891
+ return;
560
892
  }
561
- const tags = parseTags(out.split("\n"));
562
- const text = tags.length === 0 ? "No deployments found." : tags.map((tag, idx) => {
563
- return `${idx + 1}. https://${AWS_S3_BUCKET_TAMARO}/${configName}/${tag}/index.html`;
564
- }).join("\n");
565
- console.log(`${text}\n`);
893
+ const table = columnify(deployments.map((deployment, idx) => ({
894
+ "#": `${idx + 1}`,
895
+ "Deployed At": deployment.date,
896
+ Tag: deployment.tag,
897
+ Preview: createTerminalLink("🔍 Live Preview", deployment.fullUrl)
898
+ })), {
899
+ columnSplitter: " | ",
900
+ config: {
901
+ "#": { minWidth: 2 },
902
+ "Deployed At": { minWidth: 20 },
903
+ Preview: { minWidth: 16 },
904
+ Tag: { minWidth: 14 }
905
+ },
906
+ minWidth: 10
907
+ });
908
+ console.log(`\n${table}\n`);
566
909
  };
567
910
  const assertOptionsValid$4 = (options) => {
568
911
  const { config, profile } = options;
@@ -574,10 +917,6 @@ const assertConfigValid = (config) => {
574
917
  const regex = /^[\w-]+$/;
575
918
  if (!regex.test(config)) halt(`Flag "--config" has forbidden format. Allowed format: ${regex.toString()}.`);
576
919
  };
577
- const parseTags = (lines) => {
578
- const regex = /^\s*PRE\s*/;
579
- return lines.filter((line) => regex.test(line)).map((line) => line.replace(regex, "").replace(/\/$/, ""));
580
- };
581
920
  //#endregion
582
921
  //#region src/commands/serve.ts
583
922
  const serve = async (options) => {
@@ -669,7 +1008,7 @@ const undeploy = async (options) => {
669
1008
  const deployUrl = all ? `s3://${AWS_S3_BUCKET_TAMARO}/${configName}/` : `s3://${AWS_S3_BUCKET_TAMARO}/${configName}/${tag}/`;
670
1009
  const description = all ? `all tags of "${configName}"` : `tag "${tag}" of "${configName}"`;
671
1010
  if (dryRunFlag) logTitle("🧪 DRY RUN MODE - No actual changes will be made 🧪");
672
- await promptConfirmation(`Are you sure you want to undeploy ${description}?`, options.yes);
1011
+ await promptConfirmation(`Are you sure you want to undeploy ${description}?`, options.ci);
673
1012
  const cmd = `
674
1013
  aws s3 rm ${deployUrl}
675
1014
  --recursive
@@ -789,7 +1128,7 @@ const promptBucketTamaroEmailConfig = async () => {
789
1128
  //#endregion
790
1129
  //#region package.json
791
1130
  var name = "@raisenow/tamaro-cli";
792
- var version = "1.8.0-dev.7";
1131
+ var version = "1.9.0-dev.1";
793
1132
  //#endregion
794
1133
  //#region src/cli.ts
795
1134
  /**
@@ -807,7 +1146,7 @@ const cli = createCommand().name(name).version(version, "-v, --version");
807
1146
  cli.command("dev").description("Run the local development server for Tamaro Core or a particular customer configuration").option("--local-core", "Load Tamaro Core from localhost:1234 instead of the CDN", false).option("--https", "Let local web server serve Tamaro with SSL encryption", false).option("--port <port>", "Web server port", `${DEFAULT_PORT}`).option("--env <env>", "Environment (dev, stage, prod)").option("--nolint", "Disable ESLint", false).option("--debug", "Enable debug mode", false).action(async (options) => {
808
1147
  await dev(options);
809
1148
  });
810
- cli.command("build").description("Build an optimised (minified) bundle of Tamaro Core or a customer configuration").option("--local-core", "Load Tamaro Core from localhost:1234 instead of the CDN", false).option("--analyze", "Generate bundle statistics to \"reports\" folder", false).option("--serve", "Run the generated bundle with a local web server", false).option("--https", "Let local web server serve Tamaro with SSL encryption", false).option("--port <port>", "Web server port", `${DEFAULT_PORT}`).option("--env <env>", "Environment (dev, stage, prod)").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--deploy", "Deploy Tamaro Core or a customer configuration bundle to AWS S3", false).option("--tag <tag>", "Tag which should be used for the deployment of the bundle", DEFAULT_TAG).option("--nolint", "Disable ESLint", false).option("--debug", "Enable debug mode", false).action(async (options) => {
1149
+ cli.command("build").description("Build an optimised (minified) bundle of Tamaro Core or a customer configuration").option("--local-core", "Load Tamaro Core from localhost:1234 instead of the CDN", false).option("--analyze", "Generate bundle statistics to \"reports\" folder", false).option("--serve", "Run the generated bundle with a local web server", false).option("--https", "Let local web server serve Tamaro with SSL encryption", false).option("--port <port>", "Web server port", `${DEFAULT_PORT}`).option("--env <env>", "Environment (dev, stage, prod)").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--deploy", "Deploy Tamaro Core or a customer configuration bundle to AWS S3", false).option("--skip-epms-sync-only-use-in-emergencies-or-you-will-be-fired", "Skip the EPMS sync entirely. Only use this in emergencies.", false).option("--tag <tag>", "Tag which should be used for the deployment of the bundle", DEFAULT_TAG).option("--nolint", "Disable ESLint", false).option("--debug", "Enable debug mode", false).action(async (options) => {
811
1150
  await build(options);
812
1151
  if (options.serve) await serve(options);
813
1152
  if (options.deploy) await deploy(options);
@@ -815,7 +1154,7 @@ cli.command("build").description("Build an optimised (minified) bundle of Tamaro
815
1154
  cli.command("serve").description("Run a local web server for pre-built bundle").option("--port <port>", "Web server port", `${DEFAULT_PORT}`).option("--https", "Let local web server serve Tamaro with SSL encryption", false).action(async (options) => {
816
1155
  await serve(options);
817
1156
  });
818
- cli.command("deploy").description("Deploy a pre-built Tamaro Core or customer configuration bundle to AWS S3").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--tag <tag>", "Tag which should be used for the deployment of the bundle", DEFAULT_TAG).option("--dryrun", "Displays the operations that would be performed without actually running them", false).action(async (options) => {
1157
+ cli.command("deploy").description("Deploy a pre-built Tamaro Core or customer configuration bundle to AWS S3").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--tag <tag>", "Tag which should be used for the deployment of the bundle", DEFAULT_TAG).option("--dryrun", "Displays the operations that would be performed without actually running them", false).option("--skip-epms-sync-only-use-in-emergencies-or-you-will-be-fired", "Skip the EPMS sync entirely. Only use this in emergencies.", false).action(async (options) => {
819
1158
  await deploy(options);
820
1159
  });
821
1160
  cli.command("list-deployed").description("List deployments of Tamaro Core or a particular customer configuration").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--config <config>", "Configuration name (default: current customer configuration)").action(async (options) => {
@@ -836,6 +1175,9 @@ cli.command("archive").description("Archive a customer configuration by moving i
836
1175
  cli.command("unarchive").description("Unarchive a customer configuration by restoring it from the _archived folder").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--dryrun", "Displays the operations that would be performed without actually running them", false).action((options) => {
837
1176
  unarchive(options);
838
1177
  });
1178
+ cli.command("update-epms").description("Update EPMS with the currently deployed widget tags.").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--dryrun", "Displays the operations that would be performed without actually running them", false).option("--tag <tag>", "Tag which should be set for the deployment", DEFAULT_TAG).action(async (options) => {
1179
+ await updateEpms(options);
1180
+ });
839
1181
  cli.command("validate").description("Validate the current Tamaro configuration to avoid common errors").action(async () => {
840
1182
  await validate();
841
1183
  });
@@ -6,8 +6,10 @@ import prompts from "prompts";
6
6
  import stripIndent from "strip-indent";
7
7
  import chalk from "chalk";
8
8
  import columnify from "columnify";
9
+ import envPaths from "env-paths";
9
10
  import { fileURLToPath } from "node:url";
10
11
  import { getIfUtils } from "webpack-config-utils";
12
+ import { z } from "zod";
11
13
  import { globSync } from "glob";
12
14
  import { config } from "dotenv";
13
15
  //#region src/lib/logging.ts
@@ -27,6 +29,9 @@ const logDataTable = (data) => {
27
29
  console.log(columnify(data, { showHeaders: false }));
28
30
  console.log("");
29
31
  };
32
+ const createTerminalLink = (text, url) => {
33
+ return `\u001B]8;;${url}\u001B\\${text}\u001B]8;;\u001B\\`;
34
+ };
30
35
  //#endregion
31
36
  //#region src/lib/command.ts
32
37
  const halt = (message) => {
@@ -50,6 +55,26 @@ const promptConfirmation = async (message, skipConfirmation) => {
50
55
  if (!confirmed) halt("Operation cancelled.");
51
56
  };
52
57
  //#endregion
58
+ //#region src/lib/constants.ts
59
+ const DEFAULT_TAG = "latest";
60
+ const DEFAULT_PORT = 1234;
61
+ const DEFAULT_HTTP_TIMEOUT = 1e4;
62
+ const AWS_S3_BUCKET_TAMARO = "tamaro.raisenow.com";
63
+ const AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_STAGE = "rnw-stage-email-service";
64
+ const AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_PROD = "rnw-email-service";
65
+ const EPMS_API_BASE_URL_STAGE = "https://api.stage.mesos.raisenow.net";
66
+ const EPMS_API_BASE_URL_PROD = "https://api.raisenow.io";
67
+ const EPMS_AUTH_BASE_URL_STAGE = "https://login.stage.mesos.raisenow.net";
68
+ const EPMS_AUTH_BASE_URL_PROD = "https://login.raisenow.com";
69
+ const EPMS_AUTH_CLIENT_ID_STAGE = "luna-stage-local";
70
+ const EPMS_AUTH_CLIENT_ID_PROD = "luna-local";
71
+ const CORE_CONFIG_NAME = "tamaro-core";
72
+ const AUTH_PORT = 8080;
73
+ const AWS_CLOUDFRONT_DISTRIBUTION_ID = "EHJ1OM458YQ0I";
74
+ const HTTPS_CRT_FILE = "localhost.crt";
75
+ const HTTPS_KEY_FILE = "localhost.key";
76
+ const CACHE_DIR = envPaths("tamaro-cli", { suffix: "" }).cache;
77
+ //#endregion
53
78
  //#region node_modules/.pnpm/tsdown@0.21.8_synckit@0.11.11_typescript@6.0.2/node_modules/tsdown/esm-shims.js
54
79
  const getFilename = () => fileURLToPath(import.meta.url);
55
80
  const getDirname = () => path.dirname(getFilename());
@@ -145,6 +170,7 @@ const getPaths = (ifCore) => {
145
170
  appTsConfig: resolveApp("tsconfig.json"),
146
171
  appHtml: resolveApp("*.html"),
147
172
  appEnv: resolveApp(".env*"),
173
+ configYml: resolveApp("config.yml"),
148
174
  appTailwindConfig: void 0
149
175
  });
150
176
  };
@@ -171,17 +197,37 @@ const getIfCoreFns = ({ allowArchived = false } = {}) => {
171
197
  `));
172
198
  process.exit(1);
173
199
  };
174
- //#endregion
175
- //#region src/lib/constants.ts
176
- const DEFAULT_TAG = "latest";
177
- const DEFAULT_PORT = 1234;
178
- const AWS_S3_BUCKET_TAMARO = "tamaro.raisenow.com";
179
- const AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_STAGE = "rnw-stage-email-service";
180
- const AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_PROD = "rnw-email-service";
181
- const CORE_CONFIG_NAME = "tamaro-core";
182
- const AWS_CLOUDFRONT_DISTRIBUTION_ID = "EHJ1OM458YQ0I";
183
- const HTTPS_CRT_FILE = "localhost.crt";
184
- const HTTPS_KEY_FILE = "localhost.key";
200
+ const resolveAccountUuidFromConfig = (rawConfig, field) => {
201
+ const epmsConfig = z.object({ [field]: z.record(z.string(), z.unknown()) }).safeParse(rawConfig);
202
+ if (!epmsConfig.success) return [];
203
+ const configContent = epmsConfig.data[field];
204
+ const accountUuid = configContent.account_mapping ?? configContent.account_uuid;
205
+ const accountUuidValidation = z.uuid().safeParse(accountUuid);
206
+ const accontUuidArrayValidation = z.array(z.uuid()).safeParse(accountUuid);
207
+ if (!accountUuidValidation.success && !accontUuidArrayValidation.success) halt(stripIndent(`Could not extract EPMS account UUID(s) from config.yml. Please ensure that the "${field}" field is correctly formatted. Expected formats:
208
+ 1. ${field}:
209
+ account_uuid: <UUID>
210
+
211
+ Alternatively, if the account_uuid is conditional using "if" / "then" / "else" statements, it must be provided using the "account_mapping" format:
212
+ 2. ${field}:
213
+ account_mapping:
214
+ - <UUID_1>
215
+ - <UUID_2>
216
+ - <UUID_N>
217
+ `));
218
+ if (accontUuidArrayValidation.success) return accontUuidArrayValidation.data;
219
+ if (accountUuidValidation.success) return [accountUuidValidation.data];
220
+ throw new Error("This should never happen");
221
+ };
222
+ const TAMARO_VERSION_IN_URL_REGEX = /tamaro-core\/(.*)\/index.js/;
223
+ const extractTamaroVersionFromUrl = (url) => {
224
+ const match = url.match(TAMARO_VERSION_IN_URL_REGEX);
225
+ if (match) return match[1].toString();
226
+ };
227
+ const resolveCoreVersion = () => {
228
+ if (process.env.CORE_VERSION) return process.env.CORE_VERSION;
229
+ if (process.env.CORE_URL) return extractTamaroVersionFromUrl(process.env.CORE_URL);
230
+ };
185
231
  //#endregion
186
232
  //#region src/lib/env.ts
187
233
  const require = createRequire(import.meta.url);
@@ -297,4 +343,4 @@ const applyEnv = (env) => {
297
343
  if (filePath) config({ path: filePath });
298
344
  };
299
345
  //#endregion
300
- export { promptConfirmation as C, logError as D, logDataTable as E, logSuccess as O, halt as S, logCommand as T, getWidgetUuid as _, AWS_S3_BUCKET_TAMARO as a, resolveEslintPluginConfig as b, CORE_CONFIG_NAME as c, HTTPS_CRT_FILE as d, HTTPS_KEY_FILE as f, getRelativePaths as g, getPaths as h, AWS_CLOUDFRONT_DISTRIBUTION_ID as i, logTitle as k, DEFAULT_PORT as l, getIfCoreFns as m, assertEnvValid as n, AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_PROD as o, extensions as p, getEnvVars as r, AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_STAGE as s, applyEnv as t, DEFAULT_TAG as u, resolveApp as v, runCommandSync as w, resolveOwn as x, resolveBin as y };
346
+ export { EPMS_AUTH_CLIENT_ID_STAGE as A, logSuccess as B, DEFAULT_PORT as C, EPMS_AUTH_BASE_URL_PROD as D, EPMS_API_BASE_URL_STAGE as E, runCommandSync as F, createTerminalLink as I, logCommand as L, HTTPS_KEY_FILE as M, halt as N, EPMS_AUTH_BASE_URL_STAGE as O, promptConfirmation as P, logDataTable as R, DEFAULT_HTTP_TIMEOUT as S, EPMS_API_BASE_URL_PROD as T, logTitle as V, AWS_S3_BUCKET_TAMARO as _, getIfCoreFns as a, CACHE_DIR as b, getWidgetUuid as c, resolveBin as d, resolveCoreVersion as f, AWS_CLOUDFRONT_DISTRIBUTION_ID as g, AUTH_PORT as h, extensions as i, HTTPS_CRT_FILE as j, EPMS_AUTH_CLIENT_ID_PROD as k, resolveAccountUuidFromConfig as l, resolveOwn as m, assertEnvValid as n, getPaths as o, resolveEslintPluginConfig as p, getEnvVars as r, getRelativePaths as s, applyEnv as t, resolveApp as u, AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_PROD as v, DEFAULT_TAG as w, CORE_CONFIG_NAME as x, AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_STAGE as y, logError as z };
@@ -1,4 +1,4 @@
1
- import { E as logDataTable, b as resolveEslintPluginConfig, d as HTTPS_CRT_FILE, f as HTTPS_KEY_FILE, g as getRelativePaths, h as getPaths, k as logTitle, m as getIfCoreFns, p as extensions, r as getEnvVars, v as resolveApp } from "./env-CSg7BrvF.js";
1
+ import { M as HTTPS_KEY_FILE, R as logDataTable, V as logTitle, a as getIfCoreFns, i as extensions, j as HTTPS_CRT_FILE, o as getPaths, p as resolveEslintPluginConfig, r as getEnvVars, s as getRelativePaths, u as resolveApp } from "./env-JzPIlctP.js";
2
2
  import { createRequire } from "node:module";
3
3
  import { existsSync } from "node:fs";
4
4
  import path from "node:path";