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

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
@@ -187,7 +187,7 @@ Deploy a pre-built _Tamaro Core_ or customer configuration bundle to AWS S3.
187
187
 
188
188
  - `--dryrun` – Displays the operations that would be performed without actually running them
189
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.
190
+ - `--force-skip-epms-sync` – Skip the EPMS sync entirely. Only use this in emergencies.
191
191
 
192
192
  Make sure you have built the bundle before deploying it with this command, otherwise you may mistakenly deploy the
193
193
  bundle from a previous build.
package/dist/cli.js CHANGED
@@ -1,23 +1,23 @@
1
1
  #!/usr/bin/env node
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";
2
+ import { A as EPMS_OAUTH_CLIENT_ID, B as logInfo, C as DEFAULT_HTTP_TIMEOUT, D as EPMS_API_BASE_URL_STAGE, E as EPMS_API_BASE_URL_PROD, F as runCommandSync, H as logTitle, I as createTerminalLink, L as logCommand, M as HTTPS_KEY_FILE, N as halt, O as EPMS_AUTH_BASE_URL_PROD, P as promptConfirmation, R as logDataTable, S as CORE_CONFIG_NAME, T as DEFAULT_TAG, V as logSuccess, _ as AWS_CLOUDFRONT_DISTRIBUTION_ID, a as getIfCoreFns, b as AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_STAGE, c as getWidgetUuid, d as resolveBin, f as resolveCoreVersion, g as AUTH_PORT, h as resolveOwn, j as HTTPS_CRT_FILE, k as EPMS_AUTH_BASE_URL_STAGE, l as resolveAccountUuidFromConfig, m as resolveManagedByFromConfig, n as assertEnvValid, o as getPaths, t as applyEnv, v as AWS_S3_BUCKET_TAMARO, w as DEFAULT_PORT, x as CACHE_DIR, y as AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_PROD, z as logError } from "./env-DZ3cWM6p.js";
3
3
  import { createCommand } from "commander";
4
4
  import fs, { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
5
5
  import path from "node:path";
6
+ import stripIndent from "strip-indent";
6
7
  import { S3 } from "@aws-sdk/client-s3";
7
8
  import { execaCommandSync } from "execa";
8
9
  import prompts from "prompts";
9
- import stripIndent from "strip-indent";
10
10
  import columnify from "columnify";
11
- import notifier from "node-notifier";
11
+ import * as yaml from "js-yaml";
12
12
  import { z } from "zod";
13
+ import ky from "ky";
14
+ import { createServer } from "node:http";
15
+ import open from "open";
16
+ import notifier from "node-notifier";
13
17
  import { globSync } from "glob";
14
18
  import Handlebars from "handlebars";
15
19
  import helpers from "handlebars-helpers";
16
20
  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";
21
21
  import { getPortPromise } from "portfinder";
22
22
  //#region src/lib/aws.ts
23
23
  const awsAuthenticate = (options) => {
@@ -114,246 +114,6 @@ const getConfigDeployments = async (configName) => {
114
114
  return deployments;
115
115
  };
116
116
  //#endregion
117
- //#region src/lib/notifier.ts
118
- const notify = (args) => {
119
- const { message = "", title } = args;
120
- notifier.notify({
121
- contentImage: "https://assets.raisenow.io/favicon.png",
122
- message: stripIndent(message).trim(),
123
- sound: "Funk",
124
- title
125
- });
126
- };
127
- //#endregion
128
- //#region src/commands/archive.ts
129
- const archive = (options) => {
130
- const { ifCore } = getIfCoreFns();
131
- assertIsNotCore$4(ifCore);
132
- assertIsNotArchived();
133
- const configName = getWidgetUuid();
134
- const cwd = process.cwd();
135
- const parentDir = path.dirname(cwd);
136
- const archiveDir = path.resolve(parentDir, "_archived");
137
- const archiveTarget = path.resolve(archiveDir, configName);
138
- assertArchiveTargetDoesNotExist(archiveTarget);
139
- assertOptionsValid$9(options);
140
- if (options.dryrun) logTitle("🧪 DRY RUN MODE - No actual changes will be made 🧪");
141
- if (!options.dryrun) {
142
- if (!existsSync(archiveDir)) mkdirSync(archiveDir, { recursive: true });
143
- renameSync(cwd, archiveTarget);
144
- } else logTitle(`Would move ${cwd} → ${archiveTarget}`);
145
- logSuccess(`\n✅ "${configName}" has been archived and moved to ${archiveTarget}`);
146
- process.on("exit", () => {
147
- notify({
148
- message: `"${configName}" has been archived.`,
149
- title: "archive"
150
- });
151
- });
152
- };
153
- const assertOptionsValid$9 = (options) => {
154
- const { profile } = options;
155
- if (profile) assertProfileValid(profile);
156
- };
157
- const assertIsNotCore$4 = (ifCore) => {
158
- if (ifCore()) halt("You cannot archive Tamaro Core.");
159
- };
160
- const assertIsNotArchived = () => {
161
- if (path.basename(path.dirname(process.cwd())) === "_archived") halt("This configuration is already archived.");
162
- };
163
- const assertArchiveTargetDoesNotExist = (archiveTarget) => {
164
- if (existsSync(archiveTarget)) halt(`Archive target already exists: ${archiveTarget}. Please remove it first.`);
165
- };
166
- //#endregion
167
- //#region src/lib/validators/validateEmailTemplates.ts
168
- /**
169
- * Register custom Handlebars helpers - the same, which are used in the Email Service.
170
- * Make sure to update it if new helpers are added in the Email Service.
171
- */
172
- helpers.comparison({ handlebars: Handlebars });
173
- /**
174
- * Recurse through the expected email templates folder and attempt to parse
175
- * handlebars template to validate their syntax.
176
- */
177
- const validateEmailTemplates = (templatePaths) => {
178
- const errors = [];
179
- for (const templatePath of templatePaths) try {
180
- const template = fs.readFileSync(templatePath, "utf8");
181
- assureOnlySupportedHelpers(template);
182
- compileEmailTemplate(template);
183
- console.log(`${templatePath} is a valid email template`);
184
- } catch (error) {
185
- errors.push(`${templatePath} is invalid`);
186
- errors.push(error);
187
- }
188
- return errors;
189
- };
190
- /**
191
- * Validate that only supported Handlebars helpers are used in the template.
192
- * This function extracts helpers from the template AST and checks them
193
- * against a predefined set of supported helpers.
194
- */
195
- const assureOnlySupportedHelpers = (template) => {
196
- /**
197
- * Recursively extract helpers from the AST
198
- */
199
- const extractHelpers = (ast) => {
200
- const usedHelpers = /* @__PURE__ */ new Set();
201
- const extractHelperName = (node) => {
202
- if (node.path?.type === "PathExpression" && (node.params?.length > 0 || node.hash?.pairs?.length > 0)) return node.path?.original;
203
- };
204
- const visit = (node) => {
205
- if (!node) return;
206
- switch (node.type) {
207
- case "BlockStatement":
208
- case "MustacheStatement": {
209
- const name = extractHelperName(node);
210
- if (name) usedHelpers.add(name);
211
- break;
212
- }
213
- }
214
- /**
215
- * Recursively visit children of the AST node.
216
- */
217
- for (const key in node) {
218
- const value = node[key];
219
- if (Array.isArray(value)) value.forEach((v) => visit(v));
220
- else if (value && typeof value === "object" && value.type) visit(value);
221
- }
222
- };
223
- visit(ast);
224
- return usedHelpers;
225
- };
226
- const usedHelpers = extractHelpers(Handlebars.parse(template));
227
- const supportedHelpers = new Set([
228
- "if",
229
- "unless",
230
- "each",
231
- "with",
232
- "log",
233
- "lookup",
234
- "and",
235
- "compare",
236
- "contains",
237
- "default",
238
- "eq",
239
- "gt",
240
- "gte",
241
- "has",
242
- "isFalsey",
243
- "isTruthy",
244
- "ifEven",
245
- "ifNth",
246
- "ifOdd",
247
- "is",
248
- "isnt",
249
- "lt",
250
- "lte",
251
- "neither",
252
- "not",
253
- "or",
254
- "unlessEq",
255
- "unlessGt",
256
- "unlessLt",
257
- "unlessGteq",
258
- "unlessLteq"
259
- ]);
260
- const unsupportedHelpers = [...usedHelpers].filter((h) => !supportedHelpers.has(h));
261
- if (unsupportedHelpers.length > 0) throw new Error(`Email template uses unsupported Handlebars helpers: ${unsupportedHelpers.join(", ")}`);
262
- };
263
- /**
264
- * Compile Email template and pass an empty context to validate syntax.
265
- */
266
- const compileEmailTemplate = (template) => {
267
- Handlebars.compile(template)({});
268
- };
269
- //#endregion
270
- //#region src/commands/validate.ts
271
- /**
272
- * Validates the current Tamaro configuration to avoid common errors.
273
- *
274
- * The command description is kept generic on purpose. We check for a number
275
- * of mistakes observed when tamaro configurations are manually edited and
276
- * deployed.
277
- */
278
- const validate = async () => {
279
- const errors = validateEmailTemplates(globSync("email-config/templates/**/*.hbs").toSorted());
280
- if (errors.length > 0) halt(errors.map((el) => el.toString()).join("\n"));
281
- logTitle("Validated successfully");
282
- };
283
- //#endregion
284
- //#region src/lib/https.ts
285
- const assertCrtExists = () => {
286
- const { ifCore } = getIfCoreFns();
287
- const paths = getPaths(ifCore);
288
- const certFile = path.resolve(paths.root, HTTPS_CRT_FILE);
289
- const keyFile = path.resolve(paths.root, HTTPS_KEY_FILE);
290
- if (existsSync(certFile) && existsSync(keyFile)) return;
291
- halt(stripIndent(`
292
- Flag "--https" is used, but "${HTTPS_CRT_FILE}" and/or "${HTTPS_KEY_FILE}" files are not found.
293
- You need to generate certificate first.
294
- Check docs: https://unpkg.com/@raisenow/tamaro-cli/readme.html#/?id=use-transport-level-security-https
295
- `));
296
- };
297
- //#endregion
298
- //#region src/commands/build.ts
299
- const build = async (options) => {
300
- const { env } = options;
301
- applyEnv(env);
302
- if (options.deploy && !options.profile && !options.ci) {
303
- assertProfilesPresent();
304
- options.profile = await promptProfile();
305
- }
306
- assertOptionsValid$8(options);
307
- await validate();
308
- const flags = prepareFlags$2(options);
309
- const { ifCore } = getIfCoreFns();
310
- const paths = getPaths(ifCore);
311
- const configName = ifCore(CORE_CONFIG_NAME, getWidgetUuid());
312
- const title = ifCore("Building optimised (minified) bundle of Tamaro Core …", `Building optimised (minified) bundle of "${configName}" customer configuration …`);
313
- const cmd = `
314
- ${resolveBin("webpack")}
315
- --config ${resolveOwn("dist/webpack.config.js")}
316
- --env min
317
- ${flags}
318
- `;
319
- logTitle(title);
320
- logCommand(cmd);
321
- runCommandSync(cmd, { stdio: "inherit" });
322
- logTitle(`\nBundle for "${configName}" customer configuration is done.`);
323
- console.log(`Path: ${paths.appDist}\n`);
324
- process.on("exit", () => {
325
- notify({
326
- message: `Bundle for "${configName}" customer configuration is done.`,
327
- title: "build"
328
- });
329
- });
330
- };
331
- const assertOptionsValid$8 = (options) => {
332
- const { deploy, env, https, localCore, profile } = options;
333
- const { ifCore } = getIfCoreFns();
334
- if (ifCore() && localCore) halt("Flag \"--local-core\" is redundant if running in Tamaro Core context.");
335
- if (localCore && deploy) halt("Flags \"--local-core\" and \"--deploy\" must not be used together.");
336
- if (https) assertCrtExists();
337
- if (profile) assertProfileValid(profile);
338
- assertEnvValid(env);
339
- };
340
- const prepareFlags$2 = (options) => {
341
- const flags = [];
342
- const { analyze, debug, https, localCore, nolint } = options;
343
- if (localCore) flags.push("--env localCore");
344
- if (analyze) flags.push("--env analyze");
345
- if (https) flags.push("--env https");
346
- if (nolint) flags.push("--env nolint");
347
- if (debug) flags.push("--env debug");
348
- return flags.join(" ");
349
- };
350
- //#endregion
351
- //#region src/lib/validators/assertTagValid.ts
352
- const assertTagValid = (tag) => {
353
- const regex = /^[\w.-]+$/;
354
- if (!regex.test(tag)) halt(`Flag "--tag" has forbidden format. Allowed format: ${regex.toString()}.`);
355
- };
356
- //#endregion
357
117
  //#region src/lib/epms/auth/util.ts
358
118
  const CachedTokenSchema = z.object({
359
119
  expirationTime: z.number(),
@@ -538,121 +298,396 @@ const createBrowserAuthorizer = (authBaseUrl, clientId, epmsBaseUrl) => {
538
298
  } });
539
299
  };
540
300
  //#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
301
+ //#region src/lib/epms/myselfSchema.ts
302
+ const myselfSchema = z.object({
303
+ email: z.string(),
304
+ identity_roles: z.array(z.object({
305
+ identity_uuid: z.string(),
306
+ role: z.object({
307
+ name: z.string(),
308
+ uuid: z.string()
309
+ })
310
+ })),
311
+ uuid: z.string()
312
+ });
313
+ //#endregion
314
+ //#region src/lib/epms/epmsClient.ts
315
+ /**
316
+ * Manages authentication and API interactions with EPMS.
317
+ * It provides factory methods for creating instances based on different authentication flows (client credentials or browser-based) and handles token caching and renewal transparently.
318
+ */
319
+ var EpmsClient = class EpmsClient {
320
+ #cacheKey;
321
+ #epmsClient;
322
+ #authType;
323
+ /**
324
+ * Creates an instance of EpmsClient.
325
+ *
326
+ * @param authType The type of authentication used by the client.
327
+ * @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.
328
+ * @param epmsClient The underlying KyInstance used for making API requests.
329
+ */
330
+ constructor(authType, cacheKey, epmsClient) {
331
+ this.#authType = authType;
332
+ this.#cacheKey = cacheKey;
333
+ this.#epmsClient = epmsClient;
334
+ }
335
+ static fromClientCredentials(baseUrl, clientId, clientSecret) {
336
+ return new EpmsClient("client_credentials", getTokenCacheKey(baseUrl, clientId), createAuthorizer(baseUrl, clientId, clientSecret));
337
+ }
338
+ static fromBrowserAuth(epmsBaseUrl, authBaseUrl, clientId) {
339
+ return new EpmsClient("browser", getTokenCacheKey(authBaseUrl, clientId), createBrowserAuthorizer(authBaseUrl, clientId, epmsBaseUrl));
340
+ }
341
+ get authType() {
342
+ return this.#authType;
343
+ }
344
+ logout() {
345
+ removeCachedToken(this.#cacheKey);
346
+ }
347
+ async myself() {
348
+ return this.#epmsClient.get("users/myself").json().then((response) => myselfSchema.parse(response));
349
+ }
350
+ async getOrganisationIdByAccountUuid(accountUuid) {
351
+ try {
352
+ const organisationUuid = (await this.#epmsClient.get(`accounts/${accountUuid}`).json()).organisation.uuid;
353
+ if (!organisationUuid) throw new Error("Organisation UUID not found in response");
354
+ return organisationUuid;
355
+ } catch (error) {
356
+ throw new Error(`Organisation UUID not found for account UUID: ${accountUuid}`, { cause: error });
357
+ }
358
+ }
359
+ async updateTamaro(configName, tag, json) {
360
+ return this.#epmsClient.put(`products/tamaro/${configName}/tags/${tag}`, { json }).json();
361
+ }
362
+ async archiveTamaro(configName) {
363
+ return this.#epmsClient.post(`products/tamaro/${configName}/archive`).json();
364
+ }
365
+ async unarchiveTamaro(configName) {
366
+ return this.#epmsClient.post(`products/tamaro/${configName}/unarchive`).json();
367
+ }
368
+ };
369
+ //#endregion
370
+ //#region src/lib/epms/helpers.ts
371
+ const epmsAuthenticate = (env) => {
372
+ const { authBaseUrl, baseUrl, clientId, clientSecret } = {
373
+ prod: {
374
+ authBaseUrl: EPMS_AUTH_BASE_URL_PROD,
375
+ baseUrl: EPMS_API_BASE_URL_PROD,
376
+ clientId: process.env.EPMS_CLIENT_ID,
377
+ clientSecret: process.env.EPMS_CLIENT_SECRET
378
+ },
379
+ stage: {
380
+ authBaseUrl: EPMS_AUTH_BASE_URL_STAGE,
381
+ baseUrl: EPMS_API_BASE_URL_STAGE,
382
+ clientId: process.env.EPMS_CLIENT_ID_STAGE,
383
+ clientSecret: process.env.EPMS_CLIENT_SECRET_STAGE
384
+ }
385
+ }[env];
386
+ if (clientId && clientSecret) return EpmsClient.fromClientCredentials(baseUrl, clientId, clientSecret);
387
+ return EpmsClient.fromBrowserAuth(baseUrl, authBaseUrl, EPMS_OAUTH_CLIENT_ID);
388
+ };
389
+ const loadTamaroMetadataFromConfig = (paths) => {
390
+ if (!("configYml" in paths)) {
391
+ halt("Fatal error, this should never happen");
392
+ throw new Error("Fatal error");
393
+ }
394
+ const configContent = yaml.load(readFileSync(paths.configYml, "utf8"));
395
+ const accountUuidStage = resolveAccountUuidFromConfig(configContent, "epms_stage");
396
+ return {
397
+ accountUuidProd: resolveAccountUuidFromConfig(configContent, "epms"),
398
+ accountUuidStage,
399
+ managedBy: resolveManagedByFromConfig(configContent)
400
+ };
401
+ };
402
+ const assertUserIsSuperAdmin = async (epmsClient) => {
403
+ if (epmsClient.authType === "client_credentials") return;
404
+ const myself = await epmsClient.myself();
405
+ if (!myself.identity_roles.some(({ role }) => role.name === "super_admin")) {
406
+ epmsClient.logout();
407
+ logError("\nYou must be a super admin to perform this action.");
408
+ logTitle("\nYour EPMS user details:");
409
+ logDataTable({
410
+ email: myself.email,
411
+ roles: myself.identity_roles.map(({ role }) => role.name).join(", "),
412
+ uuid: myself.uuid
413
+ });
414
+ logError("If this keeps happening, go to Configurator / Hub and logout before trying again.");
415
+ halt();
416
+ }
417
+ };
418
+ //#endregion
419
+ //#region src/lib/notifier.ts
420
+ const notify = (args) => {
421
+ const { message = "", title } = args;
422
+ notifier.notify({
423
+ contentImage: "https://assets.raisenow.io/favicon.png",
424
+ message: stripIndent(message).trim(),
425
+ sound: "Funk",
426
+ title
427
+ });
428
+ };
429
+ //#endregion
430
+ //#region src/commands/archive.ts
431
+ const archive = async (options) => {
432
+ const { ifCore } = getIfCoreFns();
433
+ assertIsNotCore$4(ifCore);
434
+ assertIsNotArchived();
435
+ const configName = getWidgetUuid();
436
+ const cwd = process.cwd();
437
+ const parentDir = path.dirname(cwd);
438
+ const archiveDir = path.resolve(parentDir, "_archived");
439
+ const archiveTarget = path.resolve(archiveDir, configName);
440
+ assertArchiveTargetDoesNotExist(archiveTarget);
441
+ assertOptionsValid$9(options);
442
+ if (options.dryrun) logTitle("🧪 DRY RUN MODE - No actual changes will be made 🧪");
443
+ if (!ifCore() && !options.forceSkipEpmsSync) await archiveEpms(configName, !!options.dryrun);
444
+ if (!options.dryrun) {
445
+ if (!existsSync(archiveDir)) mkdirSync(archiveDir, { recursive: true });
446
+ renameSync(cwd, archiveTarget);
447
+ } else logTitle(`Would move ${cwd} → ${archiveTarget}`);
448
+ logSuccess(`\n✅ "${configName}" has been archived and moved to ${archiveTarget}`);
449
+ process.on("exit", () => {
450
+ notify({
451
+ message: `"${configName}" has been archived.`,
452
+ title: "archive"
453
+ });
454
+ });
455
+ };
456
+ const archiveEpms = async (configName, dryrun) => {
457
+ const { ifCore } = getIfCoreFns();
458
+ const { accountUuidProd, accountUuidStage } = loadTamaroMetadataFromConfig(getPaths(ifCore));
459
+ if (!accountUuidProd && !accountUuidStage) halt(stripIndent(`
460
+ No EPMS account UUIDs found in config.yml.
461
+ Add them to your config.yml, for example:
462
+ epms:
463
+ account_uuid: <UUID>
464
+ epms_stage:
465
+ account_uuid: <UUID>
466
+
467
+ Or re-run with --force-skip-epms-sync to skip the EPMS sync entirely - ONLY use in emergencies / or if you know what you're doing.
468
+ `));
469
+ const environments = [{
470
+ accountUuid: accountUuidStage,
471
+ env: "stage",
472
+ label: "Stage"
473
+ }, {
474
+ accountUuid: accountUuidProd,
475
+ env: "prod",
476
+ label: "Prod"
477
+ }];
478
+ for (const { accountUuid, env, label } of environments) {
479
+ if (!accountUuid) continue;
480
+ const epmsClient = epmsAuthenticate(env);
481
+ await assertUserIsSuperAdmin(epmsClient);
482
+ logTitle(`\nArchiving EPMS ${label}: ${configName}`);
483
+ if (!dryrun) {
484
+ await epmsClient.archiveTamaro(configName);
485
+ logSuccess(`✅ EPMS ${label} entry archived.`);
486
+ } else logSuccess(`⏭️ EPMS ${label} entry would be archived (dry run).`);
487
+ }
488
+ };
489
+ const assertOptionsValid$9 = (options) => {
490
+ const { profile } = options;
491
+ if (profile) assertProfileValid(profile);
492
+ };
493
+ const assertIsNotCore$4 = (ifCore) => {
494
+ if (ifCore()) halt("You cannot archive Tamaro Core.");
495
+ };
496
+ const assertIsNotArchived = () => {
497
+ if (path.basename(path.dirname(process.cwd())) === "_archived") halt("This configuration is already archived.");
498
+ };
499
+ const assertArchiveTargetDoesNotExist = (archiveTarget) => {
500
+ if (existsSync(archiveTarget)) halt(`Archive target already exists: ${archiveTarget}. Please remove it first.`);
501
+ };
502
+ //#endregion
503
+ //#region src/lib/validators/validateEmailTemplates.ts
504
+ /**
505
+ * Register custom Handlebars helpers - the same, which are used in the Email Service.
506
+ * Make sure to update it if new helpers are added in the Email Service.
507
+ */
508
+ helpers.comparison({ handlebars: Handlebars });
509
+ /**
510
+ * Recurse through the expected email templates folder and attempt to parse
511
+ * handlebars template to validate their syntax.
512
+ */
513
+ const validateEmailTemplates = (templatePaths) => {
514
+ const errors = [];
515
+ for (const templatePath of templatePaths) try {
516
+ const template = fs.readFileSync(templatePath, "utf8");
517
+ assureOnlySupportedHelpers(template);
518
+ compileEmailTemplate(template);
519
+ console.log(`${templatePath} is a valid email template`);
520
+ } catch (error) {
521
+ errors.push(`${templatePath} is invalid`);
522
+ errors.push(error);
523
+ }
524
+ return errors;
525
+ };
526
+ /**
527
+ * Validate that only supported Handlebars helpers are used in the template.
528
+ * This function extracts helpers from the template AST and checks them
529
+ * against a predefined set of supported helpers.
530
+ */
531
+ const assureOnlySupportedHelpers = (template) => {
532
+ /**
533
+ * Recursively extract helpers from the AST
534
+ */
535
+ const extractHelpers = (ast) => {
536
+ const usedHelpers = /* @__PURE__ */ new Set();
537
+ const extractHelperName = (node) => {
538
+ if (node.path?.type === "PathExpression" && (node.params?.length > 0 || node.hash?.pairs?.length > 0)) return node.path?.original;
539
+ };
540
+ const visit = (node) => {
541
+ if (!node) return;
542
+ switch (node.type) {
543
+ case "BlockStatement":
544
+ case "MustacheStatement": {
545
+ const name = extractHelperName(node);
546
+ if (name) usedHelpers.add(name);
547
+ break;
548
+ }
549
+ }
550
+ /**
551
+ * Recursively visit children of the AST node.
552
+ */
553
+ for (const key in node) {
554
+ const value = node[key];
555
+ if (Array.isArray(value)) value.forEach((v) => visit(v));
556
+ else if (value && typeof value === "object" && value.type) visit(value);
557
+ }
558
+ };
559
+ visit(ast);
560
+ return usedHelpers;
561
+ };
562
+ const usedHelpers = extractHelpers(Handlebars.parse(template));
563
+ const supportedHelpers = new Set([
564
+ "if",
565
+ "unless",
566
+ "each",
567
+ "with",
568
+ "log",
569
+ "lookup",
570
+ "and",
571
+ "compare",
572
+ "contains",
573
+ "default",
574
+ "eq",
575
+ "gt",
576
+ "gte",
577
+ "has",
578
+ "isFalsey",
579
+ "isTruthy",
580
+ "ifEven",
581
+ "ifNth",
582
+ "ifOdd",
583
+ "is",
584
+ "isnt",
585
+ "lt",
586
+ "lte",
587
+ "neither",
588
+ "not",
589
+ "or",
590
+ "unlessEq",
591
+ "unlessGt",
592
+ "unlessLt",
593
+ "unlessGteq",
594
+ "unlessLteq"
595
+ ]);
596
+ const unsupportedHelpers = [...usedHelpers].filter((h) => !supportedHelpers.has(h));
597
+ if (unsupportedHelpers.length > 0) throw new Error(`Email template uses unsupported Handlebars helpers: ${unsupportedHelpers.join(", ")}`);
598
+ };
599
+ /**
600
+ * Compile Email template and pass an empty context to validate syntax.
601
+ */
602
+ const compileEmailTemplate = (template) => {
603
+ Handlebars.compile(template)({});
604
+ };
605
+ //#endregion
606
+ //#region src/commands/validate.ts
555
607
  /**
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.
608
+ * Validates the current Tamaro configuration to avoid common errors.
609
+ *
610
+ * The command description is kept generic on purpose. We check for a number
611
+ * of mistakes observed when tamaro configurations are manually edited and
612
+ * deployed.
558
613
  */
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
- }
614
+ const validate = async () => {
615
+ const errors = validateEmailTemplates(globSync("email-config/templates/**/*.hbs").toSorted());
616
+ if (errors.length > 0) halt(errors.map((el) => el.toString()).join("\n"));
617
+ logTitle("Validated successfully");
608
618
  };
609
619
  //#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);
620
+ //#region src/lib/https.ts
621
+ const assertCrtExists = () => {
622
+ const { ifCore } = getIfCoreFns();
623
+ const paths = getPaths(ifCore);
624
+ const certFile = path.resolve(paths.root, HTTPS_CRT_FILE);
625
+ const keyFile = path.resolve(paths.root, HTTPS_KEY_FILE);
626
+ if (existsSync(certFile) && existsSync(keyFile)) return;
627
+ halt(stripIndent(`
628
+ Flag "--https" is used, but "${HTTPS_CRT_FILE}" and/or "${HTTPS_KEY_FILE}" files are not found.
629
+ You need to generate certificate first.
630
+ Check docs: https://unpkg.com/@raisenow/tamaro-cli/readme.html#/?id=use-transport-level-security-https
631
+ `));
628
632
  };
629
- const loadAccountUuidsFromConfig = (paths) => {
630
- if (!("configYml" in paths)) {
631
- halt("Fatal error, this should never happen");
632
- throw new Error("Fatal error");
633
+ //#endregion
634
+ //#region src/commands/build.ts
635
+ const build = async (options) => {
636
+ const { env } = options;
637
+ applyEnv(env);
638
+ if (options.deploy && !options.profile && !options.ci) {
639
+ assertProfilesPresent();
640
+ options.profile = await promptProfile();
633
641
  }
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
642
+ assertOptionsValid$8(options);
643
+ await validate();
644
+ const flags = prepareFlags$2(options);
645
+ const { ifCore } = getIfCoreFns();
646
+ const paths = getPaths(ifCore);
647
+ const configName = ifCore(CORE_CONFIG_NAME, getWidgetUuid());
648
+ const title = ifCore("Building optimised (minified) bundle of Tamaro Core …", `Building optimised (minified) bundle of "${configName}" customer configuration …`);
649
+ const cmd = `
650
+ ${resolveBin("webpack")}
651
+ --config ${resolveOwn("dist/webpack.config.js")}
652
+ --env min
653
+ ${flags}
654
+ `;
655
+ logTitle(title);
656
+ logCommand(cmd);
657
+ runCommandSync(cmd, { stdio: "inherit" });
658
+ logTitle(`\nBundle for "${configName}" customer configuration is done.`);
659
+ console.log(`Path: ${paths.appDist}\n`);
660
+ process.on("exit", () => {
661
+ notify({
662
+ message: `Bundle for "${configName}" customer configuration is done.`,
663
+ title: "build"
652
664
  });
653
- logError("If this keeps happening, go to Configurator / Hub and logout before trying again.");
654
- halt();
655
- }
665
+ });
666
+ };
667
+ const assertOptionsValid$8 = (options) => {
668
+ const { deploy, env, https, localCore, profile } = options;
669
+ const { ifCore } = getIfCoreFns();
670
+ if (ifCore() && localCore) halt("Flag \"--local-core\" is redundant if running in Tamaro Core context.");
671
+ if (localCore && deploy) halt("Flags \"--local-core\" and \"--deploy\" must not be used together.");
672
+ if (https) assertCrtExists();
673
+ if (profile) assertProfileValid(profile);
674
+ assertEnvValid(env);
675
+ };
676
+ const prepareFlags$2 = (options) => {
677
+ const flags = [];
678
+ const { analyze, debug, https, localCore, nolint } = options;
679
+ if (localCore) flags.push("--env localCore");
680
+ if (analyze) flags.push("--env analyze");
681
+ if (https) flags.push("--env https");
682
+ if (nolint) flags.push("--env nolint");
683
+ if (debug) flags.push("--env debug");
684
+ return flags.join(" ");
685
+ };
686
+ //#endregion
687
+ //#region src/lib/validators/assertTagValid.ts
688
+ const assertTagValid = (tag) => {
689
+ const regex = /^[\w.-]+$/;
690
+ if (!regex.test(tag)) halt(`Flag "--tag" has forbidden format. Allowed format: ${regex.toString()}.`);
656
691
  };
657
692
  //#endregion
658
693
  //#region src/commands/update-epms.ts
@@ -672,7 +707,7 @@ const updateEpms = async (options) => {
672
707
  const { dryrun, tag } = options;
673
708
  const configName = getWidgetUuid();
674
709
  loadEnv(paths);
675
- const { accountUuidProd, accountUuidStage } = loadAccountUuidsFromConfig(paths);
710
+ const { accountUuidProd, accountUuidStage, managedBy } = loadTamaroMetadataFromConfig(paths);
676
711
  if (!accountUuidProd && !accountUuidStage) {
677
712
  logTitle("No EPMS account UUIDs found in config.yml. Skipping EPMS update.");
678
713
  return;
@@ -686,6 +721,7 @@ const updateEpms = async (options) => {
686
721
  const baseInfo = {
687
722
  configured_version: configuredVersion ?? "unknown",
688
723
  last_deployment: Math.trunc(Date.now() / 1e3),
724
+ managed_by: managedBy,
689
725
  read_only: false
690
726
  };
691
727
  const environments = [{
@@ -721,8 +757,7 @@ const updateEpms = async (options) => {
721
757
  };
722
758
  const loadEnv = (paths) => {
723
759
  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({
760
+ if (filePath) config({
726
761
  path: filePath,
727
762
  quiet: true
728
763
  });
@@ -802,8 +837,8 @@ const deploy = async (options) => {
802
837
  halt(error.stderr);
803
838
  }
804
839
  }
805
- if (options.skipEpmsSyncOnlyUseInEmergenciesOrYouWillBeFired) logTitle("⚠️ EPMS sync skipped. You better know what you are doing.");
806
- else await updateEpms(options);
840
+ if (options.forceSkipEpmsSync) logTitle("⚠️ EPMS sync skipped. You better know what you are doing.");
841
+ else if (!ifCore()) await updateEpms(options);
807
842
  const demoPage = `https://${AWS_S3_BUCKET_TAMARO}/${configName}/${tag}/index.html`;
808
843
  const entryPoint = `https://${AWS_S3_BUCKET_TAMARO}/${configName}/${tag}/${entryFilename}`;
809
844
  logTitle(`\nBundle for "${configName}" is deployed with tag "${tag}".`);
@@ -1026,7 +1061,7 @@ const prepareFlags = async (options) => {
1026
1061
  };
1027
1062
  //#endregion
1028
1063
  //#region src/commands/unarchive.ts
1029
- const unarchive = (options) => {
1064
+ const unarchive = async (options) => {
1030
1065
  const { ifCore } = getIfCoreFns({ allowArchived: true });
1031
1066
  assertIsNotCore$1(ifCore);
1032
1067
  assertIsArchived();
@@ -1037,12 +1072,13 @@ const unarchive = (options) => {
1037
1072
  const restoreTarget = path.resolve(configsDir, configName);
1038
1073
  assertRestoreTargetDoesNotExist(restoreTarget);
1039
1074
  assertOptionsValid$2(options);
1075
+ if (!ifCore() && !options.forceSkipEpmsSync) await unarchiveEpms(configName, !!options.dryrun);
1040
1076
  if (options.dryrun) logTitle("🧪 DRY RUN MODE - No actual changes will be made 🧪");
1041
1077
  if (!options.dryrun) {
1042
1078
  renameSync(cwd, restoreTarget);
1043
1079
  process.chdir(restoreTarget);
1044
1080
  } else logTitle(`Would move ${cwd} → ${restoreTarget}`);
1045
- logTitle(`✅ "${configName}" has been unarchived and moved to ${restoreTarget}`);
1081
+ logSuccess(`\n✅ "${configName}" has been unarchived and moved to ${restoreTarget}`);
1046
1082
  process.on("exit", () => {
1047
1083
  notify({
1048
1084
  message: `"${configName}" has been unarchived.`,
@@ -1050,6 +1086,39 @@ const unarchive = (options) => {
1050
1086
  });
1051
1087
  });
1052
1088
  };
1089
+ const unarchiveEpms = async (configName, dryrun) => {
1090
+ const { ifCore } = getIfCoreFns({ allowArchived: true });
1091
+ const { accountUuidProd, accountUuidStage } = loadTamaroMetadataFromConfig(getPaths(ifCore));
1092
+ if (!accountUuidProd && !accountUuidStage) halt(stripIndent(`
1093
+ No EPMS account UUIDs found in config.yml.
1094
+ Add them to your config.yml, for example:
1095
+ epms:
1096
+ account_uuid: <UUID>
1097
+ epms_stage:
1098
+ account_uuid: <UUID>
1099
+
1100
+ Or re-run with --force-skip-epms-sync to skip the EPMS sync entirely - ONLY use in emergencies / or if you know what you're doing.
1101
+ `));
1102
+ const environments = [{
1103
+ accountUuid: accountUuidStage,
1104
+ env: "stage",
1105
+ label: "Stage"
1106
+ }, {
1107
+ accountUuid: accountUuidProd,
1108
+ env: "prod",
1109
+ label: "Prod"
1110
+ }];
1111
+ for (const { accountUuid, env, label } of environments) {
1112
+ if (!accountUuid) continue;
1113
+ const epmsClient = epmsAuthenticate(env);
1114
+ await assertUserIsSuperAdmin(epmsClient);
1115
+ logTitle(`\nUnarchiving EPMS ${label}: ${configName}`);
1116
+ if (!dryrun) {
1117
+ await epmsClient.unarchiveTamaro(configName);
1118
+ logSuccess(`✅ EPMS ${label} entry unarchived.`);
1119
+ } else logSuccess(`⏭️ EPMS ${label} entry would be unarchived (dry run).`);
1120
+ }
1121
+ };
1053
1122
  const assertOptionsValid$2 = (options) => {
1054
1123
  const { profile } = options;
1055
1124
  if (profile) assertProfileValid(profile);
@@ -1109,8 +1178,6 @@ const undeploy = async (options) => {
1109
1178
  halt(error.stderr);
1110
1179
  }
1111
1180
  }
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.");
1114
1181
  logTitle(`\n${description} has been undeployed.`);
1115
1182
  logDataTable({ "Removed URL:": deployUrl });
1116
1183
  process.on("exit", () => {
@@ -1120,33 +1187,6 @@ const undeploy = async (options) => {
1120
1187
  });
1121
1188
  });
1122
1189
  };
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
- };
1150
1190
  const assertOptionsValid$1 = (options) => {
1151
1191
  const { all, profile, tag } = options;
1152
1192
  const { ifCore } = getIfCoreFns();
@@ -1229,7 +1269,7 @@ const promptBucketTamaroEmailConfig = async () => {
1229
1269
  //#endregion
1230
1270
  //#region package.json
1231
1271
  var name = "@raisenow/tamaro-cli";
1232
- var version = "1.9.0-dev.2";
1272
+ var version = "2.0.0";
1233
1273
  //#endregion
1234
1274
  //#region src/cli.ts
1235
1275
  /**
@@ -1247,7 +1287,7 @@ const cli = createCommand().name(name).version(version, "-v, --version");
1247
1287
  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) => {
1248
1288
  await dev(options);
1249
1289
  });
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) => {
1290
+ 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("--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).option("--nolint", "Disable ESLint", false).option("--debug", "Enable debug mode", false).action(async (options) => {
1251
1291
  await build(options);
1252
1292
  if (options.serve) await serve(options);
1253
1293
  if (options.deploy) await deploy(options);
@@ -1255,7 +1295,7 @@ cli.command("build").description("Build an optimised (minified) bundle of Tamaro
1255
1295
  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) => {
1256
1296
  await serve(options);
1257
1297
  });
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) => {
1298
+ 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(async (options) => {
1259
1299
  await deploy(options);
1260
1300
  });
1261
1301
  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) => {
@@ -1264,17 +1304,17 @@ cli.command("list-deployed").description("List deployments of Tamaro Core or a p
1264
1304
  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) => {
1265
1305
  await deployEmailConfig(options);
1266
1306
  });
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) => {
1307
+ 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) => {
1268
1308
  await undeploy(options);
1269
1309
  });
1270
1310
  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) => {
1271
1311
  await undeployEmailConfig(options);
1272
1312
  });
1273
- 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).action((options) => {
1274
- archive(options);
1313
+ 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(async (options) => {
1314
+ await archive(options);
1275
1315
  });
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) => {
1277
- unarchive(options);
1316
+ 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(async (options) => {
1317
+ await unarchive(options);
1278
1318
  });
1279
1319
  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
1320
  await updateEpms(options);
@@ -1,9 +1,9 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { existsSync, realpathSync } from "node:fs";
3
3
  import path from "node:path";
4
+ import stripIndent from "strip-indent";
4
5
  import { execaCommandSync } from "execa";
5
6
  import prompts from "prompts";
6
- import stripIndent from "strip-indent";
7
7
  import chalk from "chalk";
8
8
  import columnify from "columnify";
9
9
  import envPaths from "env-paths";
@@ -76,8 +76,9 @@ const AWS_CLOUDFRONT_DISTRIBUTION_ID = "EHJ1OM458YQ0I";
76
76
  const HTTPS_CRT_FILE = "localhost.crt";
77
77
  const HTTPS_KEY_FILE = "localhost.key";
78
78
  const CACHE_DIR = envPaths("tamaro-cli", { suffix: "" }).cache;
79
+ const TAMARO_VERSION_IN_URL_REGEX = /tamaro-core\/(.*)\/index.js/;
79
80
  //#endregion
80
- //#region node_modules/.pnpm/tsdown@0.21.10_synckit@0.11.11_typescript@6.0.3/node_modules/tsdown/esm-shims.js
81
+ //#region node_modules/.pnpm/tsdown@0.22.0_tsx@4.19.1_typescript@6.0.3_unrun@0.2.37_synckit@0.11.11_/node_modules/tsdown/esm-shims.js
81
82
  const getFilename = () => fileURLToPath(import.meta.url);
82
83
  const getDirname = () => path.dirname(getFilename());
83
84
  const __dirname = /* @__PURE__ */ getDirname();
@@ -215,7 +216,11 @@ const resolveAccountUuidFromConfig = (rawConfig, field) => {
215
216
  `));
216
217
  return accountUuidValidation.data;
217
218
  };
218
- const TAMARO_VERSION_IN_URL_REGEX = /tamaro-core\/(.*)\/index.js/;
219
+ const resolveManagedByFromConfig = (rawConfig) => {
220
+ const managedByConfig = z.object({ managed_by: z.string().trim().min(1).max(255).optional() }).safeParse(rawConfig);
221
+ if (!managedByConfig.success) halt("Could not extract \"managed_by\" from config.yml. Please ensure it is a non-empty string.");
222
+ return managedByConfig.data?.managed_by ?? "RaiseNow";
223
+ };
219
224
  const extractTamaroVersionFromUrl = (url) => {
220
225
  const match = url.match(TAMARO_VERSION_IN_URL_REGEX);
221
226
  if (match) return match[1].toString();
@@ -339,4 +344,4 @@ const applyEnv = (env) => {
339
344
  if (filePath) config({ path: filePath });
340
345
  };
341
346
  //#endregion
342
- export { HTTPS_CRT_FILE as A, logSuccess as B, DEFAULT_PORT as C, EPMS_AUTH_BASE_URL_PROD as D, EPMS_API_BASE_URL_STAGE as E, createTerminalLink as F, logCommand as I, logDataTable as L, halt as M, promptConfirmation as N, EPMS_AUTH_BASE_URL_STAGE as O, runCommandSync as P, logError 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_KEY_FILE as j, EPMS_OAUTH_CLIENT_ID 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, logInfo as z };
347
+ export { EPMS_OAUTH_CLIENT_ID as A, logInfo as B, DEFAULT_HTTP_TIMEOUT as C, EPMS_API_BASE_URL_STAGE as D, EPMS_API_BASE_URL_PROD as E, runCommandSync as F, logTitle as H, createTerminalLink as I, logCommand as L, HTTPS_KEY_FILE as M, halt as N, EPMS_AUTH_BASE_URL_PROD as O, promptConfirmation as P, logDataTable as R, CORE_CONFIG_NAME as S, DEFAULT_TAG as T, logSuccess as V, AWS_CLOUDFRONT_DISTRIBUTION_ID as _, getIfCoreFns as a, AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_STAGE as b, getWidgetUuid as c, resolveBin as d, resolveCoreVersion as f, AUTH_PORT as g, resolveOwn as h, extensions as i, HTTPS_CRT_FILE as j, EPMS_AUTH_BASE_URL_STAGE as k, resolveAccountUuidFromConfig as l, resolveManagedByFromConfig 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 as v, DEFAULT_PORT as w, CACHE_DIR as x, AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_PROD as y, logError as z };
@@ -1,4 +1,4 @@
1
- import { A as HTTPS_CRT_FILE, L as logDataTable, V as logTitle, a as getIfCoreFns, i as extensions, j as HTTPS_KEY_FILE, o as getPaths, p as resolveEslintPluginConfig, r as getEnvVars, s as getRelativePaths, u as resolveApp } from "./env-DBP70I1v.js";
1
+ import { H as logTitle, M as HTTPS_KEY_FILE, R as logDataTable, 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-DZ3cWM6p.js";
2
2
  import { createRequire } from "node:module";
3
3
  import { existsSync } from "node:fs";
4
4
  import path from "node:path";
package/package.json CHANGED
@@ -1,15 +1,14 @@
1
1
  {
2
2
  "name": "@raisenow/tamaro-cli",
3
- "version": "1.9.0-dev.2",
3
+ "version": "2.0.0",
4
4
  "author": {
5
5
  "name": "RaiseNow",
6
6
  "email": "development@raisenow.com"
7
7
  },
8
8
  "type": "module",
9
- "bin": "dist/cli.js",
10
9
  "engines": {
11
10
  "node": ">=22.22.2",
12
- "pnpm": ">=10.33.2",
11
+ "pnpm": ">=11.0.9",
13
12
  "npm": "please-use-pnpm",
14
13
  "yarn": "please-use-pnpm"
15
14
  },
@@ -18,7 +17,7 @@
18
17
  "@babel/cli": "^7.28.6",
19
18
  "@babel/core": "^7.29.0",
20
19
  "@babel/plugin-syntax-dynamic-import": "^7.8.3",
21
- "@babel/preset-env": "^7.29.3",
20
+ "@babel/preset-env": "^7.29.5",
22
21
  "@babel/preset-react": "^7.28.5",
23
22
  "@babel/preset-typescript": "^7.28.5",
24
23
  "@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
@@ -26,7 +25,7 @@
26
25
  "@types/columnify": "^1.5.4",
27
26
  "@types/handlebars-helpers": "^0.5.6",
28
27
  "@types/lodash": "^4.17.24",
29
- "@types/node": "^22.19.17",
28
+ "@types/node": "^22.19.18",
30
29
  "@types/node-notifier": "^8.0.5",
31
30
  "@types/prompts": "^2.4.9",
32
31
  "@types/resolve-bin": "^0.4.3",
@@ -63,7 +62,7 @@
63
62
  "node-notifier": "^10.0.1",
64
63
  "open": "^11.0.0",
65
64
  "portfinder": "^1.0.38",
66
- "postcss": "^8.5.13",
65
+ "postcss": "^8.5.14",
67
66
  "postcss-import": "^16.1.1",
68
67
  "postcss-loader": "^8.2.1",
69
68
  "postcss-nested": "^7.0.2",
@@ -71,12 +70,12 @@
71
70
  "react-refresh": "^0.18.0",
72
71
  "resolve-url-loader": "^5.0.0",
73
72
  "sass": "^1.99.0",
74
- "sass-loader": "^16.0.7",
73
+ "sass-loader": "^16.0.8",
75
74
  "strip-indent": "^4.1.1",
76
75
  "style-loader": "^4.0.0",
77
- "terser-webpack-plugin": "^5.5.0",
76
+ "terser-webpack-plugin": "^5.6.0",
78
77
  "tsconfig-paths-webpack-plugin": "^4.2.0",
79
- "tsdown": "^0.21.10",
78
+ "tsdown": "^0.22.0",
80
79
  "typescript": "^6.0.3",
81
80
  "url-loader": "^4.1.1",
82
81
  "vitest": "^4.1.5",
@@ -111,5 +110,8 @@
111
110
  "lint": "DEBUG=eslint:eslint-helpers eslint .",
112
111
  "format": "prettier . --write --ignore-path ./.prettierignore",
113
112
  "test": "vitest"
113
+ },
114
+ "bin": {
115
+ "tamaro-cli": "dist/cli.js"
114
116
  }
115
117
  }
@@ -1,11 +1,11 @@
1
1
  minimumReleaseAge: 1440
2
-
3
2
  minimumReleaseAgeExclude:
4
3
  - '@raisenow/*'
5
4
  - '@rnw-npm/*'
6
-
7
- onlyBuiltDependencies:
8
- - core-js
9
- - core-js-pure
10
- - esbuild
11
- - unrs-resolver
5
+ engineStrict: true
6
+ allowBuilds:
7
+ core-js: true
8
+ core-js-pure: true
9
+ esbuild: true
10
+ highlight.js: true
11
+ unrs-resolver: true