@raisenow/tamaro-cli 3.0.0-dev.1 → 3.0.0-dev.3

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 (3) hide show
  1. package/README.md +36 -0
  2. package/dist/cli.js +199 -324
  3. package/package.json +3 -5
package/README.md CHANGED
@@ -36,6 +36,9 @@ Available commands:
36
36
  - [`unarchive`](#unarchive)
37
37
  - [`update-epms`](#update-epms)
38
38
  - [`validate`](#validate)
39
+ - [`typecheck`](#typecheck)
40
+ - [`lint`](#lint)
41
+ - [`format`](#format)
39
42
 
40
43
  ## `dev`
41
44
 
@@ -288,6 +291,39 @@ cd /path/to/tamaro-configurations/configs/epms-demo
288
291
  pnpx @raisenow/tamaro-cli validate
289
292
  ```
290
293
 
294
+ ## `typecheck`
295
+
296
+ Typecheck the current Tamaro configuration.
297
+
298
+ ### Example
299
+
300
+ ```bash
301
+ cd /path/to/tamaro-configurations/configs/epms-demo
302
+ pnpx @raisenow/tamaro-cli typecheck
303
+ ```
304
+
305
+ ## `lint`
306
+
307
+ Lint the current Tamaro configuration with ESLint.
308
+
309
+ ### Example
310
+
311
+ ```bash
312
+ cd /path/to/tamaro-configurations/configs/epms-demo
313
+ pnpx @raisenow/tamaro-cli lint
314
+ ```
315
+
316
+ ## `format`
317
+
318
+ Format the current Tamaro configuration with Prettier.
319
+
320
+ ### Example
321
+
322
+ ```bash
323
+ cd /path/to/tamaro-configurations/configs/epms-demo
324
+ pnpx @raisenow/tamaro-cli format
325
+ ```
326
+
291
327
  # Development
292
328
 
293
329
  Use the versions declared in `package.json`:
package/dist/cli.js CHANGED
@@ -1,73 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
+ import { assertAwsProfileValid, assertAwsProfilesPresent, assertCrtExists, assertTagValid, awsAuthenticate, halt, logDataTable, logError, logInfo, logSuccess, logTitle, notify, prepareAwsCliFlags, promptAwsProfile, promptConfirmation, runCommandSync } from "@rnw-npm/cli-helpers";
3
4
  import { createCommand } from "commander";
4
5
  import fs from "node:fs";
5
6
  import path from "node:path";
6
7
  import dedent from "dedent";
7
- import { S3 } from "@aws-sdk/client-s3";
8
- import { execaCommandSync } from "execa";
9
- import prompts from "prompts";
10
- import chalk from "chalk";
11
- import columnify from "columnify";
12
- import envPaths from "env-paths";
13
8
  import * as yaml from "js-yaml";
9
+ import envPaths from "env-paths";
14
10
  import { z } from "zod";
15
11
  import ky from "ky";
16
12
  import { createServer } from "node:http";
17
13
  import open from "open";
18
- import notifier from "node-notifier";
19
- import console$1 from "node:console";
20
14
  import { globSync } from "glob";
21
15
  import Handlebars from "handlebars";
22
16
  import helpers from "handlebars-helpers";
23
17
  import { config } from "dotenv";
24
18
  import { getPortPromise } from "portfinder";
25
- //#region src/lib/logging.ts
26
- const logTitle = (title) => {
27
- console.log(chalk.bold(title));
28
- };
29
- const logError = (message) => {
30
- if (message) console.log(chalk.red(message));
31
- };
32
- const logSuccess = (message) => {
33
- if (message) console.log(chalk.green(message));
34
- };
35
- const logInfo = (message) => {
36
- if (message) console.log(chalk.blue(message));
37
- };
38
- const logCommand = (cmd) => {
39
- console.log(`\n${chalk.dim(dedent(cmd))}\n`);
40
- };
41
- const logDataTable = (data) => {
42
- console.log(columnify(data, { showHeaders: false }));
43
- console.log("");
44
- };
45
- const createTerminalLink = (text, url) => {
46
- return `\u001B]8;;${url}\u001B\\${text}\u001B]8;;\u001B\\`;
47
- };
48
- //#endregion
49
- //#region src/lib/command.ts
50
- const halt = (message) => {
51
- logError(message);
52
- process.exit(1);
53
- };
54
- const prepareCommand = (cmd) => {
55
- return cmd.replaceAll("\n", " ").replaceAll(/[ \t]{2,}/g, " ").trim();
56
- };
57
- const runCommandSync = (cmd, options) => {
58
- return execaCommandSync(prepareCommand(cmd), options);
59
- };
60
- const promptConfirmation = async (message, skipConfirmation) => {
61
- if (skipConfirmation) return;
62
- const { confirmed } = await prompts([{
63
- initial: false,
64
- message,
65
- name: "confirmed",
66
- type: "confirm"
67
- }], { onCancel: () => halt() });
68
- if (!confirmed) halt("Operation cancelled.");
69
- };
70
- //#endregion
19
+ import prompts from "prompts";
20
+ import columnify from "columnify";
21
+ import { S3 } from "@aws-sdk/client-s3";
71
22
  //#region src/lib/constants.ts
72
23
  const DEFAULT_TAG = "latest";
73
24
  const DEFAULT_PORT = 1234;
@@ -75,113 +26,16 @@ const DEFAULT_HTTP_TIMEOUT = 1e4;
75
26
  const AWS_S3_BUCKET_TAMARO = "tamaro.raisenow.com";
76
27
  const AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_STAGE = "rnw-stage-email-service";
77
28
  const AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_PROD = "rnw-email-service";
29
+ const AWS_CLOUDFRONT_DISTRIBUTION_ID = "EHJ1OM458YQ0I";
78
30
  const EPMS_API_BASE_URL_STAGE = "https://api.stage.mesos.raisenow.net";
79
31
  const EPMS_API_BASE_URL_PROD = "https://api.raisenow.io";
80
32
  const EPMS_AUTH_BASE_URL_STAGE = "https://login.stage.mesos.raisenow.net";
81
33
  const EPMS_AUTH_BASE_URL_PROD = "https://login.raisenow.com";
82
34
  const EPMS_OAUTH_CLIENT_ID = "tamaro-cli";
83
35
  const AUTH_PORT = 4571;
84
- const AWS_CLOUDFRONT_DISTRIBUTION_ID = "EHJ1OM458YQ0I";
85
- const HTTPS_CRT_FILE = "localhost.crt";
86
- const HTTPS_KEY_FILE = "localhost.key";
87
36
  const CACHE_DIR = envPaths("tamaro-cli", { suffix: "" }).cache;
88
37
  const TAMARO_VERSION_IN_URL_REGEX = /tamaro-core\/(.*)\/index.js/;
89
38
  //#endregion
90
- //#region src/lib/aws.ts
91
- const awsAuthenticate = (options) => {
92
- if (options.ci)
93
- /**
94
- * Don't try to authenticate the user.
95
- * This is done when CLI is running within an automated system like pipeline,
96
- * In this case "AWS_ACCESS_KEY_ID" and "AWS_SECRET_ACCESS_KEY" are used instead of "AWS_PROFILE".
97
- */
98
- return;
99
- try {
100
- checkIdentity(options);
101
- } catch {
102
- login(options);
103
- }
104
- };
105
- const assertProfilesPresent = () => {
106
- if (getAvailableProfiles().length === 0) halt(dedent`
107
- No AWS profiles found.
108
- Run "aws configure sso" to set up SSO-enabled profile.
109
- Check the wiki for more information:
110
- https://raisenow.atlassian.net/wiki/x/lIrWvg
111
- `);
112
- };
113
- const getAvailableProfiles = () => {
114
- const { stdout } = execaCommandSync("aws configure list-profiles");
115
- return stdout.split("\n");
116
- };
117
- /**
118
- * Returns caller identity data (output is ignored here).
119
- * Fails in case of expired SSO session.
120
- */
121
- const checkIdentity = (options) => {
122
- execaCommandSync(`aws sts get-caller-identity ${prepareFlags(options)}`);
123
- };
124
- /**
125
- * Opens SSO authentication page in the browser.
126
- * Fails if user canceled authentication process.
127
- */
128
- const login = (options) => {
129
- const flags = prepareFlags(options);
130
- try {
131
- execaCommandSync(`aws sso login ${flags}`, { stdio: "inherit" });
132
- console.log("");
133
- } catch {
134
- halt("Login failed.");
135
- }
136
- };
137
- const prepareFlags = (options) => {
138
- const flags = [];
139
- const { profile } = options;
140
- if (profile) flags.push(`--profile ${profile}`);
141
- return flags.join(" ");
142
- };
143
- const assertProfileValid = (profile) => {
144
- const profiles = getAvailableProfiles();
145
- if (!profiles.includes(profile)) halt(dedent`
146
- AWS profile "${profile}" is not found.
147
- Available profiles are: ${profiles.map((v) => `"${v}"`).join(", ")}.
148
- `);
149
- };
150
- const promptProfile = async () => {
151
- const { profile } = await prompts([{
152
- choices: getAvailableProfiles().filter((v) => !!v).map((v) => ({
153
- title: v,
154
- value: v
155
- })),
156
- message: "Select AWS profile",
157
- name: "profile",
158
- type: "select"
159
- }], { onCancel: () => halt() });
160
- return profile;
161
- };
162
- const s3 = new S3({});
163
- const getConfigDeployments = async (configName) => {
164
- const widgetJs = (await s3.listObjectsV2({
165
- Bucket: "tamaro.raisenow.com",
166
- Prefix: `${configName}/`
167
- })).Contents?.filter((file) => file.Key?.endsWith(`/index.html`)) ?? [];
168
- const deployments = [];
169
- for (const file of widgetJs) {
170
- const key = file.Key ?? "";
171
- const parts = key.split("/");
172
- if (parts.length < 2 || !file.LastModified) continue;
173
- const tag = parts.at(-2);
174
- const date = file.LastModified.toISOString();
175
- const fullUrl = `https://${AWS_S3_BUCKET_TAMARO}/${key}`;
176
- deployments.push({
177
- date,
178
- fullUrl,
179
- tag
180
- });
181
- }
182
- return deployments;
183
- };
184
- //#endregion
185
39
  //#region src/lib/resolve.ts
186
40
  const resolveAccountUuidFromConfig = (rawConfig, field) => {
187
41
  const epmsConfig = z.object({ [field]: z.record(z.string(), z.unknown()) }).safeParse(rawConfig);
@@ -292,6 +146,11 @@ const createAuthorizer = (baseUrl, clientId, clientSecret) => {
292
146
  }] } });
293
147
  };
294
148
  //#endregion
149
+ //#region src/lib/logging.ts
150
+ const createTerminalLink = (text, url) => {
151
+ return `\u001B]8;;${url}\u001B\\${text}\u001B]8;;\u001B\\`;
152
+ };
153
+ //#endregion
295
154
  //#region src/lib/epms/auth/authCallback.html
296
155
  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";
297
156
  //#endregion
@@ -516,17 +375,6 @@ const assertUserIsSuperAdmin = async (epmsClient) => {
516
375
  }
517
376
  };
518
377
  //#endregion
519
- //#region src/lib/notifier.ts
520
- const notify = (args) => {
521
- const { message = "", title } = args;
522
- notifier.notify({
523
- contentImage: "https://assets.raisenow.io/favicon.png",
524
- message: dedent(message),
525
- sound: "Funk",
526
- title
527
- });
528
- };
529
- //#endregion
530
378
  //#region src/commands/archive.ts
531
379
  const archive = async (options) => {
532
380
  assertIsNotArchived();
@@ -585,7 +433,7 @@ const archiveEpms = async (configName, dryrun) => {
585
433
  };
586
434
  const assertOptionsValid$7 = (options) => {
587
435
  const { profile } = options;
588
- if (profile) assertProfileValid(profile);
436
+ if (profile) assertAwsProfileValid(profile);
589
437
  };
590
438
  const assertIsNotArchived = () => {
591
439
  if (path.basename(path.dirname(process.cwd())) === "_archived") halt("This configuration is already archived.");
@@ -780,21 +628,15 @@ const getOutDir = (configName) => {
780
628
  return path.resolve(getConfigsPackageRoot(), "dist", configName);
781
629
  };
782
630
  //#endregion
783
- //#region src/lib/validators/assertTagValid.ts
784
- const assertTagValid = (tag) => {
785
- const regex = /^[\w.-]+$/;
786
- if (!regex.test(tag)) halt(`Flag "--tag" has forbidden format. Allowed format: ${regex.toString()}.`);
787
- };
788
- //#endregion
789
631
  //#region src/commands/update-epms.ts
790
632
  const updateEpms = async (options) => {
791
633
  const configName = getConfigName();
792
634
  if (!options.profile && !options.ci) {
793
- assertProfilesPresent();
794
- options.profile = await promptProfile();
635
+ assertAwsProfilesPresent();
636
+ options.profile = await promptAwsProfile();
795
637
  }
796
638
  if (options.profile) {
797
- assertProfileValid(options.profile);
639
+ assertAwsProfileValid(options.profile);
798
640
  process.env.AWS_PROFILE = options.profile;
799
641
  }
800
642
  awsAuthenticate(options);
@@ -864,13 +706,13 @@ const deploy = async (options) => {
864
706
  const outDir = getOutDir(configName);
865
707
  assertOutDir(configName);
866
708
  if (!options.profile && !options.ci) {
867
- assertProfilesPresent();
868
- options.profile = await promptProfile();
709
+ assertAwsProfilesPresent();
710
+ options.profile = await promptAwsProfile();
869
711
  }
870
712
  assertOptionsValid$6(options);
871
713
  await validate();
872
714
  awsAuthenticate(options);
873
- const flags = prepareFlags(options);
715
+ const flags = prepareAwsCliFlags(options);
874
716
  const { dryrun, tag } = options;
875
717
  const dryRunFlag = dryrun ? `--dryrun` : "";
876
718
  const deployUrl = `s3://${AWS_S3_BUCKET_TAMARO}/${configName}/${tag}`;
@@ -879,54 +721,47 @@ const deploy = async (options) => {
879
721
  /**
880
722
  * Sync all files except entrypoint (without deleting old files)
881
723
  */
882
- const cmdSyncAll = `
883
- aws s3 sync ${outDir} ${deployUrl}
884
- --acl public-read
885
- --exclude ${outDir}/${entryFilename}
886
- --cache-control max-age=31536000
887
- ${flags}
888
- ${dryRunFlag}
889
- `;
890
- logTitle("Deploying to AWS S3...");
891
- logCommand(cmdSyncAll);
892
724
  try {
893
- runCommandSync(cmdSyncAll, { stdout: "inherit" });
725
+ logTitle("Deploying to AWS S3...");
726
+ runCommandSync(dedent`
727
+ aws s3 sync ${outDir} ${deployUrl}
728
+ --acl public-read
729
+ --exclude ${outDir}/${entryFilename}
730
+ --cache-control max-age=31536000
731
+ ${flags}
732
+ ${dryRunFlag}
733
+ `, { stdout: "inherit" });
894
734
  } catch (error) {
895
735
  halt(error.stderr);
896
736
  }
897
737
  /**
898
738
  * Copy entrypoint
899
739
  */
900
- const cmdCpEntry = `
901
- aws s3 cp ${outDir}/${entryFilename} ${deployUrl}/${entryFilename}
902
- --acl public-read
903
- --cache-control max-age=64800
904
- ${flags}
905
- ${dryRunFlag}
906
- `;
907
- logCommand(cmdCpEntry);
908
740
  try {
909
- runCommandSync(cmdCpEntry, { stdout: "inherit" });
741
+ runCommandSync(dedent`
742
+ aws s3 cp ${outDir}/${entryFilename} ${deployUrl}/${entryFilename}
743
+ --acl public-read
744
+ --cache-control max-age=64800
745
+ ${flags}
746
+ ${dryRunFlag}
747
+ `, { stdout: "inherit" });
910
748
  } catch (error) {
911
749
  halt(error.stderr);
912
750
  }
913
- if (!dryRunFlag) {
914
- /**
915
- * Invalidate cloudfront cache
916
- */
917
- const cmdInvalidateCache = `
918
- aws cloudfront create-invalidation
919
- --distribution-id ${AWS_CLOUDFRONT_DISTRIBUTION_ID}
920
- --paths /${configName}/${tag}/*
921
- ${flags}
922
- `;
751
+ if (!dryRunFlag)
752
+ /**
753
+ * Invalidate cloudfront cache
754
+ */
755
+ try {
923
756
  logTitle("\nInvalidating edge cache...");
924
- logCommand(cmdInvalidateCache);
925
- try {
926
- runCommandSync(cmdInvalidateCache);
927
- } catch (error) {
928
- halt(error.stderr);
929
- }
757
+ runCommandSync(dedent`
758
+ aws cloudfront create-invalidation
759
+ --distribution-id ${AWS_CLOUDFRONT_DISTRIBUTION_ID}
760
+ --paths /${configName}/${tag}/*
761
+ ${flags}
762
+ `);
763
+ } catch (error) {
764
+ halt(error.stderr);
930
765
  }
931
766
  if (options.forceSkipEpmsSync) logTitle("⚠️ EPMS sync skipped. You better know what you are doing.");
932
767
  else await updateEpms(options);
@@ -939,7 +774,7 @@ const deploy = async (options) => {
939
774
  });
940
775
  process.on("exit", () => {
941
776
  notify({
942
- message: dedent`
777
+ message: `
943
778
  Bundle for "${configName}" is deployed with tag "${tag}".
944
779
  Demo page: ${demoPage}
945
780
  Entry point: ${entryPoint}
@@ -950,36 +785,20 @@ const deploy = async (options) => {
950
785
  };
951
786
  const assertOptionsValid$6 = (options) => {
952
787
  const { profile, tag } = options;
953
- if (profile) assertProfileValid(profile);
788
+ if (profile) assertAwsProfileValid(profile);
954
789
  assertTagValid(tag);
955
790
  };
956
791
  //#endregion
957
- //#region src/lib/https.ts
958
- const assertCrtExists = () => {
959
- const configsPackageRoot = getConfigsPackageRoot();
960
- const certFile = path.resolve(configsPackageRoot, HTTPS_CRT_FILE);
961
- const keyFile = path.resolve(configsPackageRoot, HTTPS_KEY_FILE);
962
- if (fs.existsSync(certFile) && fs.existsSync(keyFile)) return;
963
- halt(dedent`
964
- Files "${HTTPS_CRT_FILE}" and/or "${HTTPS_KEY_FILE}" are not found.
965
- You need to generate local certificates first.
966
- 1. Install mkcert:
967
- brew install mkcert
968
- 2. Generate the certificate and key in the repository root:
969
- mkcert -install -cert-file ${HTTPS_CRT_FILE} -key-file ${HTTPS_KEY_FILE} localhost 127.0.0.1 0.0.0.0 ::1
970
- `);
971
- };
972
- //#endregion
973
792
  //#region src/commands/preview.ts
974
793
  const preview = async () => {
975
794
  const configName = getConfigName();
976
795
  const configsPackageRoot = getConfigsPackageRoot();
796
+ const relativeOutDir = path.relative(configsPackageRoot, getOutDir(configName));
977
797
  assertOutDir(configName);
978
- assertCrtExists();
979
- const cmd = `pnpm sirv ${path.relative(configsPackageRoot, getOutDir(configName))} --cors --http2 --cert ${HTTPS_CRT_FILE} --key ${HTTPS_KEY_FILE} --port ${await getPortPromise({ port: Number(DEFAULT_PORT) })}`;
980
- logTitle(`Running web-server for pre-built bundle...`);
981
- logCommand(cmd);
982
- runCommandSync(cmd, {
798
+ assertCrtExists(configsPackageRoot);
799
+ const port = await getPortPromise({ port: Number(DEFAULT_PORT) });
800
+ logTitle("\nRunning web-server for pre-built bundle...");
801
+ runCommandSync(`pnpm sirv ${relativeOutDir} --cors --http2 --cert localhost.crt --key localhost.key --port ${port}`, {
983
802
  cwd: configsPackageRoot,
984
803
  stdio: "inherit"
985
804
  });
@@ -991,27 +810,23 @@ const build = async (options) => {
991
810
  const configsPackageRoot = getConfigsPackageRoot();
992
811
  const outDir = getOutDir(configName);
993
812
  if (options.deploy && !options.profile && !options.ci) {
994
- assertProfilesPresent();
995
- options.profile = await promptProfile();
813
+ assertAwsProfilesPresent();
814
+ options.profile = await promptAwsProfile();
996
815
  }
997
816
  assertOptionsValid$5(options);
998
817
  await validate();
999
- const cmd = `pnpm vite build`;
1000
- const env = {
1001
- ...process.env,
1002
- BABEL: options.babel ? "true" : "false",
1003
- CONFIG_NAME: configName,
1004
- MINIFY: options.minify ? "true" : "false"
1005
- };
1006
818
  logTitle(`Building optimised (minified) bundle of "${configName}" customer configuration...`);
1007
- logCommand(cmd);
1008
- runCommandSync(cmd, {
819
+ runCommandSync(`pnpm vite build`, {
1009
820
  cwd: configsPackageRoot,
1010
- env,
821
+ env: {
822
+ BABEL: options.babel ? "true" : "false",
823
+ CONFIG_NAME: configName,
824
+ MINIFY: options.minify ? "true" : "false"
825
+ },
1011
826
  stdio: "inherit"
1012
827
  });
1013
828
  logTitle(`\nBundle for "${configName}" customer configuration is done.`);
1014
- console$1.log(`Path: ${outDir}\n`);
829
+ console.log(`Path: ${outDir}\n`);
1015
830
  process.on("exit", () => {
1016
831
  notify({
1017
832
  message: `Bundle for "${configName}" customer configuration is done.`,
@@ -1022,8 +837,9 @@ const build = async (options) => {
1022
837
  if (options.deploy) await deploy(options);
1023
838
  };
1024
839
  const assertOptionsValid$5 = (options) => {
1025
- const { profile } = options;
1026
- if (profile) assertProfileValid(profile);
840
+ const { preview, profile } = options;
841
+ if (preview) assertCrtExists(getConfigsPackageRoot());
842
+ if (profile) assertAwsProfileValid(profile);
1027
843
  };
1028
844
  //#endregion
1029
845
  //#region src/commands/deploy-email-config.ts
@@ -1032,28 +848,26 @@ const deployEmailConfig = async (options) => {
1032
848
  const emailConfigPath = `${getInputDir(configName)}/email-config`;
1033
849
  assertEmailConfigPathExists(emailConfigPath);
1034
850
  if (!options.profile && !options.ci) {
1035
- assertProfilesPresent();
1036
- options.profile = await promptProfile();
851
+ assertAwsProfilesPresent();
852
+ options.profile = await promptAwsProfile();
1037
853
  }
1038
854
  if (!options.bucket) options.bucket = options.ci ? AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_PROD : await promptBucketTamaroEmailConfig$1();
1039
855
  assertOptionsValid$4(options);
1040
856
  await validate();
1041
857
  awsAuthenticate(options);
1042
- const flags = prepareFlags(options);
858
+ const flags = prepareAwsCliFlags(options);
1043
859
  const dryRunFlag = options.dryrun ? `--dryrun` : "";
1044
860
  const deployUrl = `s3://${options.bucket}/${configName}`;
1045
861
  if (dryRunFlag) logTitle("🧪 DRY RUN MODE - No actual changes will be made 🧪");
1046
- const cmd = `
1047
- aws s3 sync ${emailConfigPath} ${deployUrl}
1048
- --delete
1049
- --exact-timestamps
1050
- ${flags}
1051
- ${dryRunFlag}
1052
- `;
1053
- logTitle(`Deploying email configuration for "${configName}" customer configuration to AWS S3...`);
1054
- logCommand(cmd);
1055
862
  try {
1056
- runCommandSync(cmd, { stdout: "inherit" });
863
+ logTitle(`Deploying email configuration for "${configName}" customer configuration to AWS S3...`);
864
+ runCommandSync(dedent`
865
+ aws s3 sync ${emailConfigPath} ${deployUrl}
866
+ --delete
867
+ --exact-timestamps
868
+ ${flags}
869
+ ${dryRunFlag}
870
+ `, { stdout: "inherit" });
1057
871
  } catch (error) {
1058
872
  halt(error.stderr);
1059
873
  }
@@ -1068,7 +882,7 @@ const deployEmailConfig = async (options) => {
1068
882
  };
1069
883
  const assertOptionsValid$4 = (options) => {
1070
884
  const { bucket, profile } = options;
1071
- if (profile) assertProfileValid(profile);
885
+ if (profile) assertAwsProfileValid(profile);
1072
886
  if (bucket) assertBucketValid$1(bucket);
1073
887
  };
1074
888
  const assertEmailConfigPathExists = (distPath) => {
@@ -1097,27 +911,87 @@ const promptBucketTamaroEmailConfig$1 = async () => {
1097
911
  const dev = async (options) => {
1098
912
  const configName = getConfigName();
1099
913
  const configsPackageRoot = getConfigsPackageRoot();
1100
- const cmd = `pnpm vite dev --port ${await getPortPromise({ port: Number(DEFAULT_PORT) })}`;
1101
- const env = {
1102
- ...process.env,
1103
- CONFIG_NAME: configName,
1104
- LOCAL_CORE: options.localCore ? "true" : "false"
1105
- };
914
+ assertCrtExists(configsPackageRoot);
915
+ const port = await getPortPromise({ port: Number(DEFAULT_PORT) });
1106
916
  logTitle(`\nStarting Vite dev server for "${configName}" configuration...`);
1107
- logCommand(cmd);
1108
- runCommandSync(cmd, {
917
+ runCommandSync(`pnpm vite dev --port ${port}`, {
918
+ cwd: configsPackageRoot,
919
+ env: {
920
+ CONFIG_NAME: configName,
921
+ LOCAL_CORE: options.localCore ? "true" : "false"
922
+ },
923
+ stdio: "inherit"
924
+ });
925
+ };
926
+ //#endregion
927
+ //#region src/commands/format.ts
928
+ const format = () => {
929
+ const configsPackageRoot = getConfigsPackageRoot();
930
+ const relativeCwd = path.relative(configsPackageRoot, process.cwd());
931
+ logTitle("Formatting root with Prettier...");
932
+ runCommandSync("pnpm prettier --write . \"!configs/**\"", {
933
+ cwd: configsPackageRoot,
934
+ shell: true,
935
+ stdio: "inherit"
936
+ });
937
+ logTitle(`\nFormatting config folder "${relativeCwd}" with Prettier...`);
938
+ runCommandSync(`pnpm prettier --write ${relativeCwd}`, {
939
+ cwd: configsPackageRoot,
940
+ shell: true,
941
+ stdio: "inherit"
942
+ });
943
+ };
944
+ //#endregion
945
+ //#region src/commands/lint.ts
946
+ const lint = () => {
947
+ const configsPackageRoot = getConfigsPackageRoot();
948
+ const relativeCwd = path.relative(configsPackageRoot, process.cwd());
949
+ const env = { DEBUG: "eslint:eslint-helpers" };
950
+ logTitle("Linting root with ESLint...");
951
+ runCommandSync("pnpm eslint --ignore-pattern \"configs/\" .", {
952
+ cwd: configsPackageRoot,
953
+ env,
954
+ shell: true,
955
+ stdio: "inherit"
956
+ });
957
+ logTitle(`\nLinting config folder "${relativeCwd}" with ESLint...`);
958
+ runCommandSync(`pnpm eslint ${relativeCwd}`, {
1109
959
  cwd: configsPackageRoot,
1110
960
  env,
961
+ shell: true,
1111
962
  stdio: "inherit"
1112
963
  });
1113
964
  };
1114
965
  //#endregion
966
+ //#region src/lib/aws.ts
967
+ const getConfigDeployments = async (configName) => {
968
+ const widgetJs = (await new S3({}).listObjectsV2({
969
+ Bucket: "tamaro.raisenow.com",
970
+ Prefix: `${configName}/`
971
+ })).Contents?.filter((file) => file.Key?.endsWith(`/index.html`)) ?? [];
972
+ const deployments = [];
973
+ for (const file of widgetJs) {
974
+ const key = file.Key ?? "";
975
+ const parts = key.split("/");
976
+ if (parts.length < 2 || !file.LastModified) continue;
977
+ const tag = parts.at(-2);
978
+ const date = file.LastModified.toISOString();
979
+ const fullUrl = `https://${AWS_S3_BUCKET_TAMARO}/${key}`;
980
+ deployments.push({
981
+ date,
982
+ fullUrl,
983
+ tag
984
+ });
985
+ }
986
+ return deployments;
987
+ };
988
+ //#endregion
1115
989
  //#region src/commands/list-deployed.ts
1116
990
  const listDeployed = async (options) => {
1117
991
  const configName = getConfigName();
1118
992
  if (!options.profile && !options.ci) {
1119
- assertProfilesPresent();
1120
- options.profile = await promptProfile();
993
+ assertAwsProfilesPresent();
994
+ options.profile = await promptAwsProfile();
1121
995
  }
1122
996
  assertOptionsValid$3(options);
1123
997
  awsAuthenticate(options);
@@ -1148,7 +1022,18 @@ const listDeployed = async (options) => {
1148
1022
  };
1149
1023
  const assertOptionsValid$3 = (options) => {
1150
1024
  const { profile } = options;
1151
- if (profile) assertProfileValid(profile);
1025
+ if (profile) assertAwsProfileValid(profile);
1026
+ };
1027
+ //#endregion
1028
+ //#region src/commands/typecheck.ts
1029
+ const typecheck = () => {
1030
+ const configsPackageRoot = getConfigsPackageRoot();
1031
+ const relativeCwd = path.relative(configsPackageRoot, process.cwd());
1032
+ logTitle("Running TypeScript type-checking...");
1033
+ runCommandSync(`pnpm tsc --project ${relativeCwd}/tsconfig.json`, {
1034
+ cwd: configsPackageRoot,
1035
+ stdio: "inherit"
1036
+ });
1152
1037
  };
1153
1038
  //#endregion
1154
1039
  //#region src/commands/unarchive.ts
@@ -1209,7 +1094,7 @@ const unarchiveEpms = async (configName, dryrun) => {
1209
1094
  };
1210
1095
  const assertOptionsValid$2 = (options) => {
1211
1096
  const { profile } = options;
1212
- if (profile) assertProfileValid(profile);
1097
+ if (profile) assertAwsProfileValid(profile);
1213
1098
  };
1214
1099
  const assertIsArchived = () => {
1215
1100
  if (path.basename(path.dirname(process.cwd())) !== "_archived") halt("This configuration is not archived. Only archived configurations can be unarchived.");
@@ -1222,42 +1107,39 @@ const assertRestoreTargetDoesNotExist = (restoreTarget) => {
1222
1107
  const undeploy = async (options) => {
1223
1108
  const configName = getConfigName();
1224
1109
  if (!options.profile && !options.ci) {
1225
- assertProfilesPresent();
1226
- options.profile = await promptProfile();
1110
+ assertAwsProfilesPresent();
1111
+ options.profile = await promptAwsProfile();
1227
1112
  }
1228
1113
  assertOptionsValid$1(options);
1229
1114
  awsAuthenticate(options);
1230
- const flags = prepareFlags(options);
1115
+ const flags = prepareAwsCliFlags(options);
1231
1116
  const { all, dryrun, tag = DEFAULT_TAG } = options;
1232
1117
  const dryRunFlag = dryrun ? `--dryrun` : "";
1233
1118
  const deployUrl = all ? `s3://${AWS_S3_BUCKET_TAMARO}/${configName}/` : `s3://${AWS_S3_BUCKET_TAMARO}/${configName}/${tag}/`;
1234
1119
  const description = all ? `all tags of "${configName}"` : `tag "${tag}" of "${configName}"`;
1235
1120
  if (dryRunFlag) logTitle("🧪 DRY RUN MODE - No actual changes will be made 🧪");
1236
- await promptConfirmation(`Are you sure you want to undeploy ${description}?`, options.ci);
1237
- const cmd = `
1238
- aws s3 rm ${deployUrl}
1239
- --recursive
1240
- ${flags}
1241
- ${dryRunFlag}
1242
- `;
1243
- logTitle(`Undeploying ${description} from AWS S3...`);
1244
- logCommand(cmd);
1121
+ if (!options.ci) await promptConfirmation(`Are you sure you want to undeploy ${description}?`);
1245
1122
  try {
1246
- runCommandSync(cmd, { stdout: "inherit" });
1123
+ logTitle(`Undeploying ${description} from AWS S3...`);
1124
+ runCommandSync(dedent`
1125
+ aws s3 rm ${deployUrl}
1126
+ --recursive
1127
+ ${flags}
1128
+ ${dryRunFlag}
1129
+ `, { stdout: "inherit" });
1247
1130
  } catch (error) {
1248
1131
  halt(error.stderr);
1249
1132
  }
1250
1133
  if (!dryRunFlag) {
1251
- const cmdInvalidateCache = `
1252
- aws cloudfront create-invalidation
1253
- --distribution-id ${AWS_CLOUDFRONT_DISTRIBUTION_ID}
1254
- --paths ${all ? `/${configName}/*` : `/${configName}/${tag}/*`}
1255
- ${flags}
1256
- `;
1257
- logTitle("\nInvalidating edge cache...");
1258
- logCommand(cmdInvalidateCache);
1134
+ const invalidationPath = all ? `/${configName}/*` : `/${configName}/${tag}/*`;
1259
1135
  try {
1260
- runCommandSync(cmdInvalidateCache);
1136
+ logTitle("\nInvalidating edge cache...");
1137
+ runCommandSync(dedent`
1138
+ aws cloudfront create-invalidation
1139
+ --distribution-id ${AWS_CLOUDFRONT_DISTRIBUTION_ID}
1140
+ --paths ${invalidationPath}
1141
+ ${flags}
1142
+ `);
1261
1143
  } catch (error) {
1262
1144
  halt(error.stderr);
1263
1145
  }
@@ -1273,7 +1155,7 @@ const undeploy = async (options) => {
1273
1155
  };
1274
1156
  const assertOptionsValid$1 = (options) => {
1275
1157
  const { all, profile, tag } = options;
1276
- if (profile) assertProfileValid(profile);
1158
+ if (profile) assertAwsProfileValid(profile);
1277
1159
  if (all && tag) halt("Flags \"--tag\" and \"--all\" must not be used together.");
1278
1160
  if (!all && tag) assertTagValid(tag);
1279
1161
  };
@@ -1282,8 +1164,8 @@ const assertOptionsValid$1 = (options) => {
1282
1164
  const undeployEmailConfig = async (options) => {
1283
1165
  const configName = getConfigName();
1284
1166
  if (!options.profile && !options.ci) {
1285
- assertProfilesPresent();
1286
- options.profile = await promptProfile();
1167
+ assertAwsProfilesPresent();
1168
+ options.profile = await promptAwsProfile();
1287
1169
  }
1288
1170
  if (options.ci) options.bucket = AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_PROD;
1289
1171
  if (!options.bucket) if (options.ci) options.bucket = AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_PROD;
@@ -1292,21 +1174,19 @@ const undeployEmailConfig = async (options) => {
1292
1174
  else options.bucket = await promptBucketTamaroEmailConfig();
1293
1175
  assertOptionsValid(options);
1294
1176
  awsAuthenticate(options);
1295
- const flags = prepareFlags(options);
1177
+ const flags = prepareAwsCliFlags(options);
1296
1178
  const dryRunFlag = options.dryrun ? `--dryrun` : "";
1297
1179
  const deployUrl = `s3://${options.bucket}/${configName}/`;
1298
- await promptConfirmation(`Are you sure you want to undeploy the email configuration for "${configName}" from "${options.bucket}"?`, options.ci);
1180
+ if (!options.ci) await promptConfirmation(`Are you sure you want to undeploy the email configuration for "${configName}" from "${options.bucket}"?`);
1299
1181
  if (dryRunFlag) logTitle("🧪 DRY RUN MODE - No actual changes will be made 🧪");
1300
- const cmd = `
1301
- aws s3 rm ${deployUrl}
1302
- --recursive
1303
- ${flags}
1304
- ${dryRunFlag}
1305
- `;
1306
- logTitle(`Undeploying email configuration for "${configName}" from AWS S3...`);
1307
- logCommand(cmd);
1308
1182
  try {
1309
- runCommandSync(cmd, { stdout: "inherit" });
1183
+ logTitle(`Undeploying email configuration for "${configName}" from AWS S3...`);
1184
+ runCommandSync(dedent`
1185
+ aws s3 rm ${deployUrl}
1186
+ --recursive
1187
+ ${flags}
1188
+ ${dryRunFlag}
1189
+ `, { stdout: "inherit" });
1310
1190
  } catch (error) {
1311
1191
  halt(error.stderr);
1312
1192
  }
@@ -1321,7 +1201,7 @@ const undeployEmailConfig = async (options) => {
1321
1201
  };
1322
1202
  const assertOptionsValid = (options) => {
1323
1203
  const { bucket, profile } = options;
1324
- if (profile) assertProfileValid(profile);
1204
+ if (profile) assertAwsProfileValid(profile);
1325
1205
  if (bucket) assertBucketValid(bucket);
1326
1206
  };
1327
1207
  const assertBucketValid = (bucket) => {
@@ -1345,30 +1225,25 @@ const promptBucketTamaroEmailConfig = async () => {
1345
1225
  //#endregion
1346
1226
  //#region package.json
1347
1227
  var name = "@raisenow/tamaro-cli";
1348
- var version = "3.0.0-dev.1";
1228
+ var version = "3.0.0-dev.3";
1349
1229
  //#endregion
1350
1230
  //#region src/cli.ts
1351
- /**
1352
- * Makes the script crash on unhandled rejections instead of silently
1353
- * ignoring them. In the future, promise rejections that are not handled will
1354
- * terminate the Node.js process with a non-zero exit code.
1355
- */
1356
- process.on("unhandledRejection", (error) => {
1357
- throw error;
1358
- });
1359
1231
  const cli = createCommand().name(name).description("CLI for Tamaro Customer Configurations development").version(version, "-v, --version");
1360
1232
  cli.command("dev").description("Run the local dev-server").option("--local-core", "Load Tamaro Core from \"https://localhost:1234/src/index.ts\" rather than from CDN", false).action(dev);
1361
1233
  cli.command("build").description("Build an optimised (minified) bundle").option("--preview", "Run a local web server for pre-built bundle", false).option("--babel", "Use Babel for transpilation", true).option("--no-babel", "Do not use Babel for transpilation").option("--minify", "Minify the bundle", true).option("--no-minify", "Do not minify the bundle").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--deploy", "Deploy customer configuration bundle to AWS S3", false).option("--force-skip-epms-sync", "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).action(build);
1362
1234
  cli.command("preview").description("Run a local web server for pre-built bundle").action(preview);
1363
- 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("--force-skip-epms-sync", "Skip the EPMS sync entirely. Only use this in emergencies.", false).action(deploy);
1235
+ cli.command("deploy").description("Deploy a pre-built 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("--force-skip-epms-sync", "Skip the EPMS sync entirely. Only use this in emergencies.", false).action(deploy);
1364
1236
  cli.command("list-deployed").description("List deployments of customer configuration").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).action(listDeployed);
1365
1237
  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(deployEmailConfig);
1366
- 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(undeploy);
1238
+ cli.command("undeploy").description("Undeploy a 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(undeploy);
1367
1239
  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(undeployEmailConfig);
1368
1240
  cli.command("archive").description("Archive a customer configuration by moving it to 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).option("--force-skip-epms-sync", "Skip EPMS archive sync (emergency use only)", false).action(archive);
1369
1241
  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).option("--force-skip-epms-sync", "Skip EPMS unarchive sync (emergency use only)", false).action(unarchive);
1370
1242
  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(updateEpms);
1371
1243
  cli.command("validate").description("Validate the current Tamaro configuration to avoid common errors").action(validate);
1244
+ cli.command("typecheck").description("Typecheck the current Tamaro configuration").action(typecheck);
1245
+ cli.command("lint").description("Lint the current Tamaro configuration with ESLint").action(lint);
1246
+ cli.command("format").description("Format the current Tamaro configuration with Prettier").action(format);
1372
1247
  try {
1373
1248
  logTitle(`${name} ${version}\n`);
1374
1249
  await cli.parseAsync(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raisenow/tamaro-cli",
3
- "version": "3.0.0-dev.1",
3
+ "version": "3.0.0-dev.3",
4
4
  "author": {
5
5
  "name": "RaiseNow",
6
6
  "email": "development@raisenow.com"
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@aws-sdk/client-s3": "^3.1068.0",
17
- "chalk": "^5.6.2",
17
+ "@rnw-npm/cli-helpers": "^2.0.0-dev.1",
18
18
  "columnify": "^1.6.0",
19
19
  "commander": "^14.0.3",
20
20
  "dedent": "^1.7.2",
@@ -22,11 +22,10 @@
22
22
  "env-paths": "^4.0.0",
23
23
  "execa": "^9.6.1",
24
24
  "glob": "^13.0.6",
25
- "handlebars-helpers": "^0.10.0",
26
25
  "handlebars": "^4.7.9",
26
+ "handlebars-helpers": "^0.10.0",
27
27
  "js-yaml": "^4.2.0",
28
28
  "ky": "^1.14.3",
29
- "node-notifier": "^10.0.1",
30
29
  "open": "^11.0.0",
31
30
  "portfinder": "^1.0.38",
32
31
  "prompts": "^2.4.2",
@@ -38,7 +37,6 @@
38
37
  "@types/columnify": "^1.5.4",
39
38
  "@types/handlebars-helpers": "^0.5.6",
40
39
  "@types/js-yaml": "^4.0.9",
41
- "@types/node-notifier": "^8.0.5",
42
40
  "@types/node": "^24.13.2",
43
41
  "@types/prompts": "^2.4.9",
44
42
  "eslint": "^10.5.0",