@raisenow/tamaro-cli 1.8.0 → 1.9.0-dev.2

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 HTTPS_CRT_FILE, B as logSuccess, C as DEFAULT_PORT, D as EPMS_AUTH_BASE_URL_PROD, E as EPMS_API_BASE_URL_STAGE, F as createTerminalLink, I as logCommand, L as logDataTable, M as halt, N as promptConfirmation, O as EPMS_AUTH_BASE_URL_STAGE, P as runCommandSync, R as logError, 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_KEY_FILE, k as EPMS_OAUTH_CLIENT_ID, 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 logInfo } from "./env-DBP70I1v.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,383 @@ 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 = (key) => {
368
+ const cacheKey = Buffer.from(key).toString("base64url");
369
+ return path.join(CACHE_DIR, `token-${cacheKey}.json`);
370
+ };
371
+ const getTokenCacheKey = (baseUrl, clientId) => {
372
+ return `${baseUrl}|${clientId}`;
373
+ };
374
+ const readCachedToken = (key) => {
375
+ try {
376
+ const filePath = getCacheFilePath(key);
377
+ if (!existsSync(filePath)) return;
378
+ const data = JSON.parse(readFileSync(filePath, "utf8"));
379
+ const parsed = CachedTokenSchema.safeParse(data);
380
+ if (parsed.success) return parsed.data;
381
+ } catch {}
382
+ };
383
+ const writeCachedToken = (key, cached) => {
384
+ try {
385
+ const filePath = getCacheFilePath(key);
386
+ mkdirSync(path.dirname(filePath), { recursive: true });
387
+ writeFileSync(filePath, JSON.stringify(cached), { mode: 384 });
388
+ } catch {}
389
+ };
390
+ const removeCachedToken = (key) => {
391
+ try {
392
+ const filePath = getCacheFilePath(key);
393
+ if (existsSync(filePath)) writeFileSync(filePath, "", { flag: "w" });
394
+ } catch {}
395
+ };
396
+ //#endregion
397
+ //#region src/lib/epms/auth/apiClient.ts
398
+ const createAuthorizer = (baseUrl, clientId, clientSecret) => {
399
+ const apiUnauthorized = ky.extend({
400
+ headers: { "Access-Control-Allow-Origin": "*" },
401
+ prefixUrl: baseUrl,
402
+ retry: { limit: 3 },
403
+ timeout: DEFAULT_HTTP_TIMEOUT
404
+ });
405
+ let expirationTime = void 0;
406
+ let token = void 0;
407
+ const cacheKey = getTokenCacheKey(baseUrl, clientId);
408
+ const cached = readCachedToken(cacheKey);
409
+ if (cached) {
410
+ token = cached.token;
411
+ expirationTime = cached.expirationTime;
412
+ }
413
+ const authorize = async () => {
414
+ const isExpired = (expirationTime ?? 0) - Date.now() < 300 * 1e3;
415
+ if (token && !isExpired) return token;
416
+ const data = await apiUnauthorized.post("oauth2/token", { json: {
417
+ client_id: clientId,
418
+ client_secret: clientSecret,
419
+ grant_type: "client_credentials"
420
+ } }).json();
421
+ token = `${data.token_type} ${data.access_token}`;
422
+ expirationTime = Date.now() + data.expires_in * 1e3;
423
+ writeCachedToken(cacheKey, {
424
+ expirationTime,
425
+ token
426
+ });
427
+ return token;
428
+ };
429
+ return apiUnauthorized.extend({ hooks: { beforeRequest: [async (request) => {
430
+ const authToken = await authorize();
431
+ request.headers.set("Authorization", authToken);
432
+ }] } });
433
+ };
434
+ //#endregion
435
+ //#region src/lib/epms/auth/authCallback.html
436
+ 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";
437
+ //#endregion
438
+ //#region src/lib/epms/auth/browser.ts
439
+ /**
440
+ * The following authentication method is a browser-based OAuth flow.
441
+ * A very similar flow is used by tools like AWS CLI when running `aws sso login` or Claude Code.
442
+ *
443
+ * The flow works as follows:
444
+ * 1. The CLI starts a temporary local HTTP server that listens for the OAuth callback.
445
+ * 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.
446
+ * 3. The user authenticates in the browser, and the OAuth server redirects back to the local server with the authorization token.
447
+ * 4. The local server captures the token and resolves the promise, completing the authentication flow.
448
+ *
449
+ *
450
+ * We also implement a simple caching mechanism that stores the token in a file in the user's home directory.
451
+ * This allows us to reuse the token for subsequent requests until it expires, at which point we automatically trigger a new authentication flow.
452
+ */
453
+ const doBrowserOAuthFlow = (authBaseUrl, clientId, port) => {
454
+ return new Promise((resolve, reject) => {
455
+ const server = createServer((req, res) => {
456
+ if (req.method === "POST" && req.url === "/token") {
457
+ let body = "";
458
+ req.on("data", (chunk) => {
459
+ body += chunk.toString();
460
+ });
461
+ req.on("end", () => {
462
+ try {
463
+ const data = AuthResponseSchema.parse(JSON.parse(body));
464
+ res.writeHead(200);
465
+ res.end();
466
+ server.close();
467
+ resolve(data);
468
+ } catch (error) {
469
+ res.writeHead(400);
470
+ res.end();
471
+ reject(error instanceof Error ? error : new Error(String(error)));
472
+ }
473
+ });
474
+ } else {
475
+ res.writeHead(200, { "Content-Type": "text/html" });
476
+ res.end(authCallback_default);
477
+ }
478
+ });
479
+ server.on("error", reject);
480
+ server.listen(port, () => {
481
+ const redirectUri = `http://localhost:${port}`;
482
+ const authUrl = `${authBaseUrl}/oauth2/authorize?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=token`;
483
+ logInfo("\nℹ️ Log in with your super admin account in the browser to continue.");
484
+ console.log(`Opening browser for authentication...`);
485
+ console.log(createTerminalLink(authUrl, authUrl));
486
+ open(authUrl).catch((error) => {
487
+ reject(error instanceof Error ? error : new Error(String(error)));
488
+ });
489
+ });
490
+ });
491
+ };
492
+ const createBrowserAuthorizer = (authBaseUrl, clientId, epmsBaseUrl) => {
493
+ const apiBase = ky.extend({
494
+ prefixUrl: epmsBaseUrl,
495
+ retry: { limit: 3 },
496
+ timeout: DEFAULT_HTTP_TIMEOUT
497
+ });
498
+ let expirationTime = void 0;
499
+ let token = void 0;
500
+ const cacheKey = getTokenCacheKey(authBaseUrl, clientId);
501
+ const cached = readCachedToken(cacheKey);
502
+ if (cached) {
503
+ token = cached.token;
504
+ expirationTime = cached.expirationTime;
505
+ }
506
+ const resetAuth = () => {
507
+ token = void 0;
508
+ expirationTime = void 0;
509
+ removeCachedToken(cacheKey);
510
+ };
511
+ const authorize = async () => {
512
+ const isExpired = (expirationTime ?? 0) - Date.now() < 300 * 1e3;
513
+ if (token && !isExpired) return token;
514
+ const data = await doBrowserOAuthFlow(authBaseUrl, clientId, AUTH_PORT);
515
+ token = `${data.token_type} ${data.access_token}`;
516
+ expirationTime = Date.now() + data.expires_in * 1e3;
517
+ writeCachedToken(cacheKey, {
518
+ expirationTime,
519
+ token
520
+ });
521
+ return token;
522
+ };
523
+ return apiBase.extend({ hooks: {
524
+ afterResponse: [async (request, options, response) => {
525
+ if (response.status === 401) {
526
+ resetAuth();
527
+ const newToken = await authorize();
528
+ const newRequest = new Request(request, { headers: new Headers(request.headers) });
529
+ newRequest.headers.set("Authorization", newToken);
530
+ return ky(newRequest, options);
531
+ }
532
+ return response;
533
+ }],
534
+ beforeRequest: [async (request) => {
535
+ const authToken = await authorize();
536
+ request.headers.set("Authorization", authToken);
537
+ }]
538
+ } });
539
+ };
540
+ //#endregion
541
+ //#region src/lib/epms/myselfSchema.ts
542
+ const myselfSchema = z.object({
543
+ email: z.string(),
544
+ identity_roles: z.array(z.object({
545
+ identity_uuid: z.string(),
546
+ role: z.object({
547
+ name: z.string(),
548
+ uuid: z.string()
549
+ })
550
+ })),
551
+ uuid: z.string()
552
+ });
553
+ //#endregion
554
+ //#region src/lib/epms/epmsClient.ts
555
+ /**
556
+ * Manages authentication and API interactions with EPMS.
557
+ * It provides factory methods for creating instances based on different authentication flows (client credentials or browser-based) and handles token caching and renewal transparently.
558
+ */
559
+ var EpmsClient = class EpmsClient {
560
+ #cacheKey;
561
+ #epmsClient;
562
+ #authType;
563
+ /**
564
+ * Creates an instance of EpmsClient.
565
+ *
566
+ * @param authType The type of authentication used by the client.
567
+ * @param cacheKey They key used for caching the token so that the client can manage the cache (e.g. clear it on logout). It should be unique per EPMS environment and client ID.
568
+ * @param epmsClient The underlying KyInstance used for making API requests.
569
+ */
570
+ constructor(authType, cacheKey, epmsClient) {
571
+ this.#authType = authType;
572
+ this.#cacheKey = cacheKey;
573
+ this.#epmsClient = epmsClient;
574
+ }
575
+ static fromClientCredentials(baseUrl, clientId, clientSecret) {
576
+ return new EpmsClient("client_credentials", getTokenCacheKey(baseUrl, clientId), createAuthorizer(baseUrl, clientId, clientSecret));
577
+ }
578
+ static fromBrowserAuth(epmsBaseUrl, authBaseUrl, clientId) {
579
+ return new EpmsClient("browser", getTokenCacheKey(authBaseUrl, clientId), createBrowserAuthorizer(authBaseUrl, clientId, epmsBaseUrl));
580
+ }
581
+ get authType() {
582
+ return this.#authType;
583
+ }
584
+ logout() {
585
+ removeCachedToken(this.#cacheKey);
586
+ }
587
+ async myself() {
588
+ return this.#epmsClient.get("users/myself").json().then((response) => myselfSchema.parse(response));
589
+ }
590
+ async getOrganisationIdByAccountUuid(accountUuid) {
591
+ const query = {
592
+ from: 0,
593
+ query: { $and: [{ $term: { object_uuid: accountUuid } }, { $term: { object: "account" } }] },
594
+ size: 1
595
+ };
596
+ const response = await this.#epmsClient.post("search/events", { json: query }).json();
597
+ if (response.hits.length === 0) throw new Error(`No organisation found for account UUID: ${accountUuid}`);
598
+ const organisationId = response.hits[0].organisation_uuid;
599
+ if (!organisationId) throw new Error(`Organisation UUID not found for account UUID: ${accountUuid}`);
600
+ return organisationId;
601
+ }
602
+ async updateTamaro(configName, tag, json) {
603
+ return this.#epmsClient.put(`products/tamaro/${configName}/tags/${tag}`, { json }).json();
604
+ }
605
+ async archiveTamaro(configName, tag) {
606
+ return this.#epmsClient.post(`products/tamaro/${configName}/tags/${tag}/archive`).json();
607
+ }
608
+ };
609
+ //#endregion
610
+ //#region src/lib/epms/helpers.ts
611
+ const epmsAuthenticate = (env) => {
612
+ const { authBaseUrl, baseUrl, clientId, clientSecret } = {
613
+ prod: {
614
+ authBaseUrl: EPMS_AUTH_BASE_URL_PROD,
615
+ baseUrl: EPMS_API_BASE_URL_PROD,
616
+ clientId: process.env.EPMS_CLIENT_ID,
617
+ clientSecret: process.env.EPMS_CLIENT_SECRET
618
+ },
619
+ stage: {
620
+ authBaseUrl: EPMS_AUTH_BASE_URL_STAGE,
621
+ baseUrl: EPMS_API_BASE_URL_STAGE,
622
+ clientId: process.env.EPMS_CLIENT_ID_STAGE,
623
+ clientSecret: process.env.EPMS_CLIENT_SECRET_STAGE
624
+ }
625
+ }[env];
626
+ if (clientId && clientSecret) return EpmsClient.fromClientCredentials(baseUrl, clientId, clientSecret);
627
+ return EpmsClient.fromBrowserAuth(baseUrl, authBaseUrl, EPMS_OAUTH_CLIENT_ID);
628
+ };
629
+ const loadAccountUuidsFromConfig = (paths) => {
630
+ if (!("configYml" in paths)) {
631
+ halt("Fatal error, this should never happen");
632
+ throw new Error("Fatal error");
633
+ }
634
+ const configContent = yaml.load(readFileSync(paths.configYml, "utf8"));
635
+ const accountUuidStage = resolveAccountUuidFromConfig(configContent, "epms_stage");
636
+ return {
637
+ accountUuidProd: resolveAccountUuidFromConfig(configContent, "epms"),
638
+ accountUuidStage
639
+ };
640
+ };
641
+ const assertUserIsSuperAdmin = async (epmsClient) => {
642
+ if (epmsClient.authType === "client_credentials") return;
643
+ const myself = await epmsClient.myself();
644
+ if (!myself.identity_roles.some(({ role }) => role.name === "super_admin")) {
645
+ epmsClient.logout();
646
+ logError("\nYou must be a super admin to perform this action.");
647
+ logTitle("\nYour EPMS user details:");
648
+ logDataTable({
649
+ email: myself.email,
650
+ roles: myself.identity_roles.map(({ role }) => role.name).join(", "),
651
+ uuid: myself.uuid
652
+ });
653
+ logError("If this keeps happening, go to Configurator / Hub and logout before trying again.");
654
+ halt();
655
+ }
656
+ };
657
+ //#endregion
658
+ //#region src/commands/update-epms.ts
659
+ const updateEpms = async (options) => {
660
+ const { ifCore } = getIfCoreFns();
661
+ assertIsNotCore$3(ifCore);
662
+ if (!options.profile && !options.ci) {
663
+ assertProfilesPresent();
664
+ options.profile = await promptProfile();
665
+ }
666
+ if (options.profile) {
667
+ assertProfileValid(options.profile);
668
+ process.env.AWS_PROFILE = options.profile;
669
+ }
670
+ awsAuthenticate(options);
671
+ const paths = getPaths(ifCore);
672
+ const { dryrun, tag } = options;
673
+ const configName = getWidgetUuid();
674
+ loadEnv(paths);
675
+ const { accountUuidProd, accountUuidStage } = loadAccountUuidsFromConfig(paths);
676
+ if (!accountUuidProd && !accountUuidStage) {
677
+ logTitle("No EPMS account UUIDs found in config.yml. Skipping EPMS update.");
678
+ return;
679
+ }
680
+ if (dryrun) logTitle("🧪 DRY RUN MODE - No actual changes will be made 🧪");
681
+ const configuredVersion = resolveCoreVersion();
682
+ if (!configuredVersion) {
683
+ 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.");
684
+ logTitle("This may be expected behaviour if you are using a custom build of Tamaro Core. If so, you can safely ignore this warning. ⚠️");
685
+ }
686
+ const baseInfo = {
687
+ configured_version: configuredVersion ?? "unknown",
688
+ last_deployment: Math.trunc(Date.now() / 1e3),
689
+ read_only: false
690
+ };
691
+ const environments = [{
692
+ accountUuid: accountUuidStage,
693
+ env: "stage",
694
+ label: "Stage"
695
+ }, {
696
+ accountUuid: accountUuidProd,
697
+ env: "prod",
698
+ label: "Prod"
699
+ }];
700
+ for (const { accountUuid, env, label } of environments) {
701
+ if (!accountUuid) continue;
702
+ const epmsClient = epmsAuthenticate(env);
703
+ await assertUserIsSuperAdmin(epmsClient);
704
+ const organisationUuid = await epmsClient.getOrganisationIdByAccountUuid(accountUuid);
705
+ const body = {
706
+ ...baseInfo,
707
+ account_uuid: accountUuid,
708
+ organisation_uuid: organisationUuid
709
+ };
710
+ logTitle(`\nUpdating EPMS ${label}:`);
711
+ logDataTable({
712
+ name: configName,
713
+ tag,
714
+ ...body
715
+ });
716
+ if (!dryrun) {
717
+ await epmsClient.updateTamaro(configName, tag, body);
718
+ logSuccess(`✅ EPMS ${label} is updated.`);
719
+ } else logSuccess(`⏭️ EPMS ${label} would be updated (dry run).`);
720
+ }
721
+ };
722
+ const loadEnv = (paths) => {
723
+ const filePath = globSync(paths.appEnv).find((file) => path.basename(file) === ".env");
724
+ if (!filePath) halt("No \".env\" file found. Are you in the folder of the Tamaro configuration?");
725
+ config({
726
+ path: filePath,
727
+ quiet: true
728
+ });
729
+ };
730
+ const assertIsNotCore$3 = (ifCore) => {
731
+ if (ifCore()) halt("You cannot update EPMS in the context of Tamaro Core.");
732
+ };
733
+ //#endregion
327
734
  //#region src/commands/deploy.ts
328
735
  const deploy = async (options) => {
329
736
  const { ifCore } = getIfCoreFns();
@@ -395,6 +802,8 @@ const deploy = async (options) => {
395
802
  halt(error.stderr);
396
803
  }
397
804
  }
805
+ if (options.skipEpmsSyncOnlyUseInEmergenciesOrYouWillBeFired) logTitle("⚠️ EPMS sync skipped. You better know what you are doing.");
806
+ else await updateEpms(options);
398
807
  const demoPage = `https://${AWS_S3_BUCKET_TAMARO}/${configName}/${tag}/index.html`;
399
808
  const entryPoint = `https://${AWS_S3_BUCKET_TAMARO}/${configName}/${tag}/${entryFilename}`;
400
809
  logTitle(`\nBundle for "${configName}" is deployed with tag "${tag}".`);
@@ -543,26 +952,32 @@ const listDeployed = async (options) => {
543
952
  }
544
953
  assertOptionsValid$4(options);
545
954
  awsAuthenticate(options);
546
- const flags = prepareFlags$3(options);
547
- const { config } = options;
955
+ const { config, profile } = options;
548
956
  const { ifCore } = getIfCoreFns();
549
957
  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);
958
+ logTitle(!config && ifCore() ? `Listing deployments of Tamaro Core …` : `Listing deployments of "${configName}" customer configuration …`);
959
+ if (profile) process.env.AWS_PROFILE = profile;
960
+ const deployments = await getConfigDeployments(configName);
961
+ if (deployments.length === 0) {
962
+ console.log("\nNo deployments found.\n");
963
+ return;
560
964
  }
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`);
965
+ const table = columnify(deployments.map((deployment, idx) => ({
966
+ "#": `${idx + 1}`,
967
+ "Deployed At": deployment.date,
968
+ Tag: deployment.tag,
969
+ Preview: createTerminalLink("🔍 Live Preview", deployment.fullUrl)
970
+ })), {
971
+ columnSplitter: " | ",
972
+ config: {
973
+ "#": { minWidth: 2 },
974
+ "Deployed At": { minWidth: 20 },
975
+ Preview: { minWidth: 16 },
976
+ Tag: { minWidth: 14 }
977
+ },
978
+ minWidth: 10
979
+ });
980
+ console.log(`\n${table}\n`);
566
981
  };
567
982
  const assertOptionsValid$4 = (options) => {
568
983
  const { config, profile } = options;
@@ -574,10 +989,6 @@ const assertConfigValid = (config) => {
574
989
  const regex = /^[\w-]+$/;
575
990
  if (!regex.test(config)) halt(`Flag "--config" has forbidden format. Allowed format: ${regex.toString()}.`);
576
991
  };
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
992
  //#endregion
582
993
  //#region src/commands/serve.ts
583
994
  const serve = async (options) => {
@@ -669,7 +1080,7 @@ const undeploy = async (options) => {
669
1080
  const deployUrl = all ? `s3://${AWS_S3_BUCKET_TAMARO}/${configName}/` : `s3://${AWS_S3_BUCKET_TAMARO}/${configName}/${tag}/`;
670
1081
  const description = all ? `all tags of "${configName}"` : `tag "${tag}" of "${configName}"`;
671
1082
  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);
1083
+ await promptConfirmation(`Are you sure you want to undeploy ${description}?`, options.ci);
673
1084
  const cmd = `
674
1085
  aws s3 rm ${deployUrl}
675
1086
  --recursive
@@ -698,6 +1109,8 @@ const undeploy = async (options) => {
698
1109
  halt(error.stderr);
699
1110
  }
700
1111
  }
1112
+ if (!dryRunFlag && !all && !ifCore() && !options.skipEpmsSyncOnlyUseInEmergenciesOrYouWillBeFired) await archiveEpms(configName, tag, false);
1113
+ else if (all && !ifCore()) logTitle("⚠️ EPMS archive skipped: --all flag used. Archive entries in EPMS manually if needed.");
701
1114
  logTitle(`\n${description} has been undeployed.`);
702
1115
  logDataTable({ "Removed URL:": deployUrl });
703
1116
  process.on("exit", () => {
@@ -707,6 +1120,33 @@ const undeploy = async (options) => {
707
1120
  });
708
1121
  });
709
1122
  };
1123
+ const archiveEpms = async (configName, tag, dryrun) => {
1124
+ const { ifCore } = getIfCoreFns();
1125
+ const { accountUuidProd, accountUuidStage } = loadAccountUuidsFromConfig(getPaths(ifCore));
1126
+ if (!accountUuidProd && !accountUuidStage) {
1127
+ logTitle("No EPMS account UUIDs found in config.yml. Skipping EPMS archive.");
1128
+ return;
1129
+ }
1130
+ const environments = [{
1131
+ accountUuid: accountUuidStage,
1132
+ env: "stage",
1133
+ label: "Stage"
1134
+ }, {
1135
+ accountUuid: accountUuidProd,
1136
+ env: "prod",
1137
+ label: "Prod"
1138
+ }];
1139
+ for (const { accountUuid, env, label } of environments) {
1140
+ if (!accountUuid) continue;
1141
+ const epmsClient = epmsAuthenticate(env);
1142
+ await assertUserIsSuperAdmin(epmsClient);
1143
+ logTitle(`\nArchiving EPMS ${label}: ${configName}/${tag}`);
1144
+ if (!dryrun) {
1145
+ await epmsClient.archiveTamaro(configName, tag);
1146
+ logSuccess(`✅ EPMS ${label} entry archived.`);
1147
+ } else logSuccess(`⏭️ EPMS ${label} entry would be archived (dry run).`);
1148
+ }
1149
+ };
710
1150
  const assertOptionsValid$1 = (options) => {
711
1151
  const { all, profile, tag } = options;
712
1152
  const { ifCore } = getIfCoreFns();
@@ -789,7 +1229,7 @@ const promptBucketTamaroEmailConfig = async () => {
789
1229
  //#endregion
790
1230
  //#region package.json
791
1231
  var name = "@raisenow/tamaro-cli";
792
- var version = "1.8.0";
1232
+ var version = "1.9.0-dev.2";
793
1233
  //#endregion
794
1234
  //#region src/cli.ts
795
1235
  /**
@@ -807,7 +1247,7 @@ const cli = createCommand().name(name).version(version, "-v, --version");
807
1247
  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
1248
  await dev(options);
809
1249
  });
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) => {
1250
+ 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
1251
  await build(options);
812
1252
  if (options.serve) await serve(options);
813
1253
  if (options.deploy) await deploy(options);
@@ -815,7 +1255,7 @@ cli.command("build").description("Build an optimised (minified) bundle of Tamaro
815
1255
  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
1256
  await serve(options);
817
1257
  });
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) => {
1258
+ 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
1259
  await deploy(options);
820
1260
  });
821
1261
  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) => {
@@ -824,7 +1264,7 @@ cli.command("list-deployed").description("List deployments of Tamaro Core or a p
824
1264
  cli.command("deploy-email-config").description("Deploy a widget's email configuration to AWS S3").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--bucket <bucket>", "AWS bucket for email configs").option("--dryrun", "Displays the operations that would be performed without actually running them", false).action(async (options) => {
825
1265
  await deployEmailConfig(options);
826
1266
  });
827
- cli.command("undeploy").description("Undeploy a Tamaro Core or customer configuration bundle from AWS S3").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--tag <tag>", "Tag which should be undeployed").option("--all", "Undeploy all tags", false).option("--dryrun", "Displays the operations that would be performed without actually running them", false).action(async (options) => {
1267
+ cli.command("undeploy").description("Undeploy a Tamaro Core or customer configuration bundle from AWS S3").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--tag <tag>", "Tag which should be undeployed").option("--all", "Undeploy all tags", false).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 EPMS archive sync (emergency use only)", false).action(async (options) => {
828
1268
  await undeploy(options);
829
1269
  });
830
1270
  cli.command("undeploy-email-config").description("Undeploy a widget's email configuration from AWS S3").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--bucket <bucket>", "AWS bucket for email configs").option("--stage", "Undeploy from stage environment (alternative to --bucket)").option("--prod", "Undeploy from prod environment (alternative to --bucket)").option("--dryrun", "Displays the operations that would be performed without actually running them", false).action(async (options) => {
@@ -836,6 +1276,9 @@ cli.command("archive").description("Archive a customer configuration by moving i
836
1276
  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
1277
  unarchive(options);
838
1278
  });
1279
+ 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) => {
1280
+ await updateEpms(options);
1281
+ });
839
1282
  cli.command("validate").description("Validate the current Tamaro configuration to avoid common errors").action(async () => {
840
1283
  await validate();
841
1284
  });