@raisenow/tamaro-cli 1.8.0 → 1.9.0-dev.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/cli.js +473 -30
- package/dist/{env-CSg7BrvF.js → env-DBP70I1v.js} +55 -13
- package/dist/webpack.config.js +17 -1
- package/package.json +20 -13
- package/.husky/pre-commit +0 -6
- package/.idea/inspectionProfiles/Project_Default.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/tamaro-cli.iml +0 -12
- package/.idea/vcs.xml +0 -6
- package/.idea/workspace.xml +0 -55
- package/.nvmrc +0 -1
- package/.prettierignore +0 -8
- package/bitbucket-pipelines.yml +0 -40
- package/eslint.config.ts +0 -30
- package/out-tsc/lib/tsconfig.lib.tsbuildinfo +0 -1
- package/out-tsc/node/eslint.config.d.ts +0 -5
- package/out-tsc/node/eslint.config.d.ts.map +0 -1
- package/out-tsc/node/tsconfig.node.tsbuildinfo +0 -1
- package/out-tsc/node/tsdown.config.d.ts +0 -3
- package/out-tsc/node/tsdown.config.d.ts.map +0 -1
- package/out-tsc/node/vitest.config.d.ts +0 -3
- package/out-tsc/node/vitest.config.d.ts.map +0 -1
- package/out-tsc/spec/tsconfig.spec.tsbuildinfo +0 -1
- package/renovate.json +0 -3
- package/tsconfig.base.json +0 -25
- package/tsconfig.lib.json +0 -25
- package/tsconfig.node.json +0 -18
- package/tsconfig.spec.json +0 -29
- package/tsdown.config.ts +0 -13
- package/vitest.config.ts +0 -20
package/README.md
CHANGED
|
@@ -185,7 +185,9 @@ Deploy a pre-built _Tamaro Core_ or customer configuration bundle to AWS S3.
|
|
|
185
185
|
|
|
186
186
|
### Options
|
|
187
187
|
|
|
188
|
+
- `--dryrun` – Displays the operations that would be performed without actually running them
|
|
188
189
|
- `--tag <tag>` – Tag which should be used for the deployment of the bundle (default: "latest")
|
|
190
|
+
- `--skip-epms-sync-only-use-in-emergencies-or-you-will-be-fired` – Skip the EPMS sync entirely. Only use this in emergencies.
|
|
189
191
|
|
|
190
192
|
Make sure you have built the bundle before deploying it with this command, otherwise you may mistakenly deploy the
|
|
191
193
|
bundle from a previous build.
|
|
@@ -287,6 +289,15 @@ cd /path/to/tamaro-configurations/configs/example-02-typical-customisations
|
|
|
287
289
|
npx -y @raisenow/tamaro-cli undeploy-email-config
|
|
288
290
|
```
|
|
289
291
|
|
|
292
|
+
## `update-epms`
|
|
293
|
+
|
|
294
|
+
Sync the Tamaro configuration with EPMS (without deploying it).
|
|
295
|
+
|
|
296
|
+
## Options
|
|
297
|
+
|
|
298
|
+
- `--dryrun` – Displays the operations that would be performed without actually running them
|
|
299
|
+
- `--tag <tag>` – Tag which should be used for the deployment of the bundle (default: "latest")
|
|
300
|
+
|
|
290
301
|
## `archive`
|
|
291
302
|
|
|
292
303
|
Archive a customer configuration. This moves the configuration folder into the `_archived` folder.
|
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as
|
|
2
|
+
import { A as HTTPS_CRT_FILE, B as logSuccess, C as DEFAULT_PORT, D as EPMS_AUTH_BASE_URL_PROD, E as EPMS_API_BASE_URL_STAGE, F as createTerminalLink, I as logCommand, L as logDataTable, M as halt, N as promptConfirmation, O as EPMS_AUTH_BASE_URL_STAGE, P as runCommandSync, R as logError, S as DEFAULT_HTTP_TIMEOUT, T as EPMS_API_BASE_URL_PROD, V as logTitle, _ as AWS_S3_BUCKET_TAMARO, a as getIfCoreFns, b as CACHE_DIR, c as getWidgetUuid, d as resolveBin, f as resolveCoreVersion, g as AWS_CLOUDFRONT_DISTRIBUTION_ID, h as AUTH_PORT, j as HTTPS_KEY_FILE, k as EPMS_OAUTH_CLIENT_ID, l as resolveAccountUuidFromConfig, m as resolveOwn, n as assertEnvValid, o as getPaths, t as applyEnv, v as AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_PROD, w as DEFAULT_TAG, x as CORE_CONFIG_NAME, y as AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_STAGE, z as logInfo } from "./env-DBP70I1v.js";
|
|
3
3
|
import { createCommand } from "commander";
|
|
4
|
-
import fs, { existsSync, mkdirSync, renameSync } from "node:fs";
|
|
4
|
+
import fs, { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { S3 } from "@aws-sdk/client-s3";
|
|
6
7
|
import { execaCommandSync } from "execa";
|
|
7
8
|
import prompts from "prompts";
|
|
8
9
|
import stripIndent from "strip-indent";
|
|
10
|
+
import columnify from "columnify";
|
|
9
11
|
import notifier from "node-notifier";
|
|
12
|
+
import { z } from "zod";
|
|
10
13
|
import { globSync } from "glob";
|
|
11
14
|
import Handlebars from "handlebars";
|
|
12
15
|
import helpers from "handlebars-helpers";
|
|
16
|
+
import { config } from "dotenv";
|
|
17
|
+
import * as yaml from "js-yaml";
|
|
18
|
+
import ky from "ky";
|
|
19
|
+
import { createServer } from "node:http";
|
|
20
|
+
import open from "open";
|
|
13
21
|
import { getPortPromise } from "portfinder";
|
|
14
22
|
//#region src/lib/aws.ts
|
|
15
23
|
const awsAuthenticate = (options) => {
|
|
@@ -83,6 +91,28 @@ const promptProfile = async () => {
|
|
|
83
91
|
}], { onCancel: () => halt() });
|
|
84
92
|
return profile;
|
|
85
93
|
};
|
|
94
|
+
const s3 = new S3({});
|
|
95
|
+
const getConfigDeployments = async (configName) => {
|
|
96
|
+
const widgetJs = (await s3.listObjectsV2({
|
|
97
|
+
Bucket: "tamaro.raisenow.com",
|
|
98
|
+
Prefix: `${configName}/`
|
|
99
|
+
})).Contents?.filter((file) => file.Key?.endsWith(`/index.html`)) ?? [];
|
|
100
|
+
const deployments = [];
|
|
101
|
+
for (const file of widgetJs) {
|
|
102
|
+
const key = file.Key ?? "";
|
|
103
|
+
const parts = key.split("/");
|
|
104
|
+
if (parts.length < 2 || !file.LastModified) continue;
|
|
105
|
+
const tag = parts.at(-2);
|
|
106
|
+
const date = file.LastModified.toISOString();
|
|
107
|
+
const fullUrl = `https://${AWS_S3_BUCKET_TAMARO}/${key}`;
|
|
108
|
+
deployments.push({
|
|
109
|
+
date,
|
|
110
|
+
fullUrl,
|
|
111
|
+
tag
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return deployments;
|
|
115
|
+
};
|
|
86
116
|
//#endregion
|
|
87
117
|
//#region src/lib/notifier.ts
|
|
88
118
|
const notify = (args) => {
|
|
@@ -98,7 +128,7 @@ const notify = (args) => {
|
|
|
98
128
|
//#region src/commands/archive.ts
|
|
99
129
|
const archive = (options) => {
|
|
100
130
|
const { ifCore } = getIfCoreFns();
|
|
101
|
-
assertIsNotCore$
|
|
131
|
+
assertIsNotCore$4(ifCore);
|
|
102
132
|
assertIsNotArchived();
|
|
103
133
|
const configName = getWidgetUuid();
|
|
104
134
|
const cwd = process.cwd();
|
|
@@ -124,7 +154,7 @@ const assertOptionsValid$9 = (options) => {
|
|
|
124
154
|
const { profile } = options;
|
|
125
155
|
if (profile) assertProfileValid(profile);
|
|
126
156
|
};
|
|
127
|
-
const assertIsNotCore$
|
|
157
|
+
const assertIsNotCore$4 = (ifCore) => {
|
|
128
158
|
if (ifCore()) halt("You cannot archive Tamaro Core.");
|
|
129
159
|
};
|
|
130
160
|
const assertIsNotArchived = () => {
|
|
@@ -324,6 +354,383 @@ const assertTagValid = (tag) => {
|
|
|
324
354
|
if (!regex.test(tag)) halt(`Flag "--tag" has forbidden format. Allowed format: ${regex.toString()}.`);
|
|
325
355
|
};
|
|
326
356
|
//#endregion
|
|
357
|
+
//#region src/lib/epms/auth/util.ts
|
|
358
|
+
const CachedTokenSchema = z.object({
|
|
359
|
+
expirationTime: z.number(),
|
|
360
|
+
token: z.string()
|
|
361
|
+
});
|
|
362
|
+
const AuthResponseSchema = z.object({
|
|
363
|
+
access_token: z.string(),
|
|
364
|
+
expires_in: z.number(),
|
|
365
|
+
token_type: z.string()
|
|
366
|
+
});
|
|
367
|
+
const getCacheFilePath = (key) => {
|
|
368
|
+
const cacheKey = Buffer.from(key).toString("base64url");
|
|
369
|
+
return path.join(CACHE_DIR, `token-${cacheKey}.json`);
|
|
370
|
+
};
|
|
371
|
+
const getTokenCacheKey = (baseUrl, clientId) => {
|
|
372
|
+
return `${baseUrl}|${clientId}`;
|
|
373
|
+
};
|
|
374
|
+
const readCachedToken = (key) => {
|
|
375
|
+
try {
|
|
376
|
+
const filePath = getCacheFilePath(key);
|
|
377
|
+
if (!existsSync(filePath)) return;
|
|
378
|
+
const data = JSON.parse(readFileSync(filePath, "utf8"));
|
|
379
|
+
const parsed = CachedTokenSchema.safeParse(data);
|
|
380
|
+
if (parsed.success) return parsed.data;
|
|
381
|
+
} catch {}
|
|
382
|
+
};
|
|
383
|
+
const writeCachedToken = (key, cached) => {
|
|
384
|
+
try {
|
|
385
|
+
const filePath = getCacheFilePath(key);
|
|
386
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
387
|
+
writeFileSync(filePath, JSON.stringify(cached), { mode: 384 });
|
|
388
|
+
} catch {}
|
|
389
|
+
};
|
|
390
|
+
const removeCachedToken = (key) => {
|
|
391
|
+
try {
|
|
392
|
+
const filePath = getCacheFilePath(key);
|
|
393
|
+
if (existsSync(filePath)) writeFileSync(filePath, "", { flag: "w" });
|
|
394
|
+
} catch {}
|
|
395
|
+
};
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/lib/epms/auth/apiClient.ts
|
|
398
|
+
const createAuthorizer = (baseUrl, clientId, clientSecret) => {
|
|
399
|
+
const apiUnauthorized = ky.extend({
|
|
400
|
+
headers: { "Access-Control-Allow-Origin": "*" },
|
|
401
|
+
prefixUrl: baseUrl,
|
|
402
|
+
retry: { limit: 3 },
|
|
403
|
+
timeout: DEFAULT_HTTP_TIMEOUT
|
|
404
|
+
});
|
|
405
|
+
let expirationTime = void 0;
|
|
406
|
+
let token = void 0;
|
|
407
|
+
const cacheKey = getTokenCacheKey(baseUrl, clientId);
|
|
408
|
+
const cached = readCachedToken(cacheKey);
|
|
409
|
+
if (cached) {
|
|
410
|
+
token = cached.token;
|
|
411
|
+
expirationTime = cached.expirationTime;
|
|
412
|
+
}
|
|
413
|
+
const authorize = async () => {
|
|
414
|
+
const isExpired = (expirationTime ?? 0) - Date.now() < 300 * 1e3;
|
|
415
|
+
if (token && !isExpired) return token;
|
|
416
|
+
const data = await apiUnauthorized.post("oauth2/token", { json: {
|
|
417
|
+
client_id: clientId,
|
|
418
|
+
client_secret: clientSecret,
|
|
419
|
+
grant_type: "client_credentials"
|
|
420
|
+
} }).json();
|
|
421
|
+
token = `${data.token_type} ${data.access_token}`;
|
|
422
|
+
expirationTime = Date.now() + data.expires_in * 1e3;
|
|
423
|
+
writeCachedToken(cacheKey, {
|
|
424
|
+
expirationTime,
|
|
425
|
+
token
|
|
426
|
+
});
|
|
427
|
+
return token;
|
|
428
|
+
};
|
|
429
|
+
return apiUnauthorized.extend({ hooks: { beforeRequest: [async (request) => {
|
|
430
|
+
const authToken = await authorize();
|
|
431
|
+
request.headers.set("Authorization", authToken);
|
|
432
|
+
}] } });
|
|
433
|
+
};
|
|
434
|
+
//#endregion
|
|
435
|
+
//#region src/lib/epms/auth/authCallback.html
|
|
436
|
+
var authCallback_default = "<!doctype html>\n<html>\n <head\n ><title>Authenticating</title></head\n >\n <body>\n <p>Authenticating</p>\n <script>\n const hash = window.location.hash.substring(1)\n const params = new URLSearchParams(hash)\n const data = {\n access_token: params.get('access_token'),\n token_type: params.get('token_type'),\n expires_in: parseInt(params.get('expires_in') || '0', 10),\n }\n fetch('/token', {\n method: 'POST',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify(data),\n })\n .then(() => {\n document.querySelector('p').textContent =\n 'Authentication successful! You can close this tab.'\n })\n .catch(() => {\n document.querySelector('p').textContent =\n 'Authentication failed. Please try again.'\n })\n <\/script>\n </body>\n</html>\n";
|
|
437
|
+
//#endregion
|
|
438
|
+
//#region src/lib/epms/auth/browser.ts
|
|
439
|
+
/**
|
|
440
|
+
* The following authentication method is a browser-based OAuth flow.
|
|
441
|
+
* A very similar flow is used by tools like AWS CLI when running `aws sso login` or Claude Code.
|
|
442
|
+
*
|
|
443
|
+
* The flow works as follows:
|
|
444
|
+
* 1. The CLI starts a temporary local HTTP server that listens for the OAuth callback.
|
|
445
|
+
* 2. The CLI opens the user's default web browser and navigates to the EPMS authorization URL, passing the local server's callback URL as the redirect_uri.
|
|
446
|
+
* 3. The user authenticates in the browser, and the OAuth server redirects back to the local server with the authorization token.
|
|
447
|
+
* 4. The local server captures the token and resolves the promise, completing the authentication flow.
|
|
448
|
+
*
|
|
449
|
+
*
|
|
450
|
+
* We also implement a simple caching mechanism that stores the token in a file in the user's home directory.
|
|
451
|
+
* This allows us to reuse the token for subsequent requests until it expires, at which point we automatically trigger a new authentication flow.
|
|
452
|
+
*/
|
|
453
|
+
const doBrowserOAuthFlow = (authBaseUrl, clientId, port) => {
|
|
454
|
+
return new Promise((resolve, reject) => {
|
|
455
|
+
const server = createServer((req, res) => {
|
|
456
|
+
if (req.method === "POST" && req.url === "/token") {
|
|
457
|
+
let body = "";
|
|
458
|
+
req.on("data", (chunk) => {
|
|
459
|
+
body += chunk.toString();
|
|
460
|
+
});
|
|
461
|
+
req.on("end", () => {
|
|
462
|
+
try {
|
|
463
|
+
const data = AuthResponseSchema.parse(JSON.parse(body));
|
|
464
|
+
res.writeHead(200);
|
|
465
|
+
res.end();
|
|
466
|
+
server.close();
|
|
467
|
+
resolve(data);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
res.writeHead(400);
|
|
470
|
+
res.end();
|
|
471
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
} else {
|
|
475
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
476
|
+
res.end(authCallback_default);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
server.on("error", reject);
|
|
480
|
+
server.listen(port, () => {
|
|
481
|
+
const redirectUri = `http://localhost:${port}`;
|
|
482
|
+
const authUrl = `${authBaseUrl}/oauth2/authorize?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=token`;
|
|
483
|
+
logInfo("\nℹ️ Log in with your super admin account in the browser to continue.");
|
|
484
|
+
console.log(`Opening browser for authentication...`);
|
|
485
|
+
console.log(createTerminalLink(authUrl, authUrl));
|
|
486
|
+
open(authUrl).catch((error) => {
|
|
487
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
};
|
|
492
|
+
const createBrowserAuthorizer = (authBaseUrl, clientId, epmsBaseUrl) => {
|
|
493
|
+
const apiBase = ky.extend({
|
|
494
|
+
prefixUrl: epmsBaseUrl,
|
|
495
|
+
retry: { limit: 3 },
|
|
496
|
+
timeout: DEFAULT_HTTP_TIMEOUT
|
|
497
|
+
});
|
|
498
|
+
let expirationTime = void 0;
|
|
499
|
+
let token = void 0;
|
|
500
|
+
const cacheKey = getTokenCacheKey(authBaseUrl, clientId);
|
|
501
|
+
const cached = readCachedToken(cacheKey);
|
|
502
|
+
if (cached) {
|
|
503
|
+
token = cached.token;
|
|
504
|
+
expirationTime = cached.expirationTime;
|
|
505
|
+
}
|
|
506
|
+
const resetAuth = () => {
|
|
507
|
+
token = void 0;
|
|
508
|
+
expirationTime = void 0;
|
|
509
|
+
removeCachedToken(cacheKey);
|
|
510
|
+
};
|
|
511
|
+
const authorize = async () => {
|
|
512
|
+
const isExpired = (expirationTime ?? 0) - Date.now() < 300 * 1e3;
|
|
513
|
+
if (token && !isExpired) return token;
|
|
514
|
+
const data = await doBrowserOAuthFlow(authBaseUrl, clientId, AUTH_PORT);
|
|
515
|
+
token = `${data.token_type} ${data.access_token}`;
|
|
516
|
+
expirationTime = Date.now() + data.expires_in * 1e3;
|
|
517
|
+
writeCachedToken(cacheKey, {
|
|
518
|
+
expirationTime,
|
|
519
|
+
token
|
|
520
|
+
});
|
|
521
|
+
return token;
|
|
522
|
+
};
|
|
523
|
+
return apiBase.extend({ hooks: {
|
|
524
|
+
afterResponse: [async (request, options, response) => {
|
|
525
|
+
if (response.status === 401) {
|
|
526
|
+
resetAuth();
|
|
527
|
+
const newToken = await authorize();
|
|
528
|
+
const newRequest = new Request(request, { headers: new Headers(request.headers) });
|
|
529
|
+
newRequest.headers.set("Authorization", newToken);
|
|
530
|
+
return ky(newRequest, options);
|
|
531
|
+
}
|
|
532
|
+
return response;
|
|
533
|
+
}],
|
|
534
|
+
beforeRequest: [async (request) => {
|
|
535
|
+
const authToken = await authorize();
|
|
536
|
+
request.headers.set("Authorization", authToken);
|
|
537
|
+
}]
|
|
538
|
+
} });
|
|
539
|
+
};
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/lib/epms/myselfSchema.ts
|
|
542
|
+
const myselfSchema = z.object({
|
|
543
|
+
email: z.string(),
|
|
544
|
+
identity_roles: z.array(z.object({
|
|
545
|
+
identity_uuid: z.string(),
|
|
546
|
+
role: z.object({
|
|
547
|
+
name: z.string(),
|
|
548
|
+
uuid: z.string()
|
|
549
|
+
})
|
|
550
|
+
})),
|
|
551
|
+
uuid: z.string()
|
|
552
|
+
});
|
|
553
|
+
//#endregion
|
|
554
|
+
//#region src/lib/epms/epmsClient.ts
|
|
555
|
+
/**
|
|
556
|
+
* Manages authentication and API interactions with EPMS.
|
|
557
|
+
* It provides factory methods for creating instances based on different authentication flows (client credentials or browser-based) and handles token caching and renewal transparently.
|
|
558
|
+
*/
|
|
559
|
+
var EpmsClient = class EpmsClient {
|
|
560
|
+
#cacheKey;
|
|
561
|
+
#epmsClient;
|
|
562
|
+
#authType;
|
|
563
|
+
/**
|
|
564
|
+
* Creates an instance of EpmsClient.
|
|
565
|
+
*
|
|
566
|
+
* @param authType The type of authentication used by the client.
|
|
567
|
+
* @param cacheKey They key used for caching the token so that the client can manage the cache (e.g. clear it on logout). It should be unique per EPMS environment and client ID.
|
|
568
|
+
* @param epmsClient The underlying KyInstance used for making API requests.
|
|
569
|
+
*/
|
|
570
|
+
constructor(authType, cacheKey, epmsClient) {
|
|
571
|
+
this.#authType = authType;
|
|
572
|
+
this.#cacheKey = cacheKey;
|
|
573
|
+
this.#epmsClient = epmsClient;
|
|
574
|
+
}
|
|
575
|
+
static fromClientCredentials(baseUrl, clientId, clientSecret) {
|
|
576
|
+
return new EpmsClient("client_credentials", getTokenCacheKey(baseUrl, clientId), createAuthorizer(baseUrl, clientId, clientSecret));
|
|
577
|
+
}
|
|
578
|
+
static fromBrowserAuth(epmsBaseUrl, authBaseUrl, clientId) {
|
|
579
|
+
return new EpmsClient("browser", getTokenCacheKey(authBaseUrl, clientId), createBrowserAuthorizer(authBaseUrl, clientId, epmsBaseUrl));
|
|
580
|
+
}
|
|
581
|
+
get authType() {
|
|
582
|
+
return this.#authType;
|
|
583
|
+
}
|
|
584
|
+
logout() {
|
|
585
|
+
removeCachedToken(this.#cacheKey);
|
|
586
|
+
}
|
|
587
|
+
async myself() {
|
|
588
|
+
return this.#epmsClient.get("users/myself").json().then((response) => myselfSchema.parse(response));
|
|
589
|
+
}
|
|
590
|
+
async getOrganisationIdByAccountUuid(accountUuid) {
|
|
591
|
+
const query = {
|
|
592
|
+
from: 0,
|
|
593
|
+
query: { $and: [{ $term: { object_uuid: accountUuid } }, { $term: { object: "account" } }] },
|
|
594
|
+
size: 1
|
|
595
|
+
};
|
|
596
|
+
const response = await this.#epmsClient.post("search/events", { json: query }).json();
|
|
597
|
+
if (response.hits.length === 0) throw new Error(`No organisation found for account UUID: ${accountUuid}`);
|
|
598
|
+
const organisationId = response.hits[0].organisation_uuid;
|
|
599
|
+
if (!organisationId) throw new Error(`Organisation UUID not found for account UUID: ${accountUuid}`);
|
|
600
|
+
return organisationId;
|
|
601
|
+
}
|
|
602
|
+
async updateTamaro(configName, tag, json) {
|
|
603
|
+
return this.#epmsClient.put(`products/tamaro/${configName}/tags/${tag}`, { json }).json();
|
|
604
|
+
}
|
|
605
|
+
async archiveTamaro(configName, tag) {
|
|
606
|
+
return this.#epmsClient.post(`products/tamaro/${configName}/tags/${tag}/archive`).json();
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
//#endregion
|
|
610
|
+
//#region src/lib/epms/helpers.ts
|
|
611
|
+
const epmsAuthenticate = (env) => {
|
|
612
|
+
const { authBaseUrl, baseUrl, clientId, clientSecret } = {
|
|
613
|
+
prod: {
|
|
614
|
+
authBaseUrl: EPMS_AUTH_BASE_URL_PROD,
|
|
615
|
+
baseUrl: EPMS_API_BASE_URL_PROD,
|
|
616
|
+
clientId: process.env.EPMS_CLIENT_ID,
|
|
617
|
+
clientSecret: process.env.EPMS_CLIENT_SECRET
|
|
618
|
+
},
|
|
619
|
+
stage: {
|
|
620
|
+
authBaseUrl: EPMS_AUTH_BASE_URL_STAGE,
|
|
621
|
+
baseUrl: EPMS_API_BASE_URL_STAGE,
|
|
622
|
+
clientId: process.env.EPMS_CLIENT_ID_STAGE,
|
|
623
|
+
clientSecret: process.env.EPMS_CLIENT_SECRET_STAGE
|
|
624
|
+
}
|
|
625
|
+
}[env];
|
|
626
|
+
if (clientId && clientSecret) return EpmsClient.fromClientCredentials(baseUrl, clientId, clientSecret);
|
|
627
|
+
return EpmsClient.fromBrowserAuth(baseUrl, authBaseUrl, EPMS_OAUTH_CLIENT_ID);
|
|
628
|
+
};
|
|
629
|
+
const loadAccountUuidsFromConfig = (paths) => {
|
|
630
|
+
if (!("configYml" in paths)) {
|
|
631
|
+
halt("Fatal error, this should never happen");
|
|
632
|
+
throw new Error("Fatal error");
|
|
633
|
+
}
|
|
634
|
+
const configContent = yaml.load(readFileSync(paths.configYml, "utf8"));
|
|
635
|
+
const accountUuidStage = resolveAccountUuidFromConfig(configContent, "epms_stage");
|
|
636
|
+
return {
|
|
637
|
+
accountUuidProd: resolveAccountUuidFromConfig(configContent, "epms"),
|
|
638
|
+
accountUuidStage
|
|
639
|
+
};
|
|
640
|
+
};
|
|
641
|
+
const assertUserIsSuperAdmin = async (epmsClient) => {
|
|
642
|
+
if (epmsClient.authType === "client_credentials") return;
|
|
643
|
+
const myself = await epmsClient.myself();
|
|
644
|
+
if (!myself.identity_roles.some(({ role }) => role.name === "super_admin")) {
|
|
645
|
+
epmsClient.logout();
|
|
646
|
+
logError("\nYou must be a super admin to perform this action.");
|
|
647
|
+
logTitle("\nYour EPMS user details:");
|
|
648
|
+
logDataTable({
|
|
649
|
+
email: myself.email,
|
|
650
|
+
roles: myself.identity_roles.map(({ role }) => role.name).join(", "),
|
|
651
|
+
uuid: myself.uuid
|
|
652
|
+
});
|
|
653
|
+
logError("If this keeps happening, go to Configurator / Hub and logout before trying again.");
|
|
654
|
+
halt();
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
//#endregion
|
|
658
|
+
//#region src/commands/update-epms.ts
|
|
659
|
+
const updateEpms = async (options) => {
|
|
660
|
+
const { ifCore } = getIfCoreFns();
|
|
661
|
+
assertIsNotCore$3(ifCore);
|
|
662
|
+
if (!options.profile && !options.ci) {
|
|
663
|
+
assertProfilesPresent();
|
|
664
|
+
options.profile = await promptProfile();
|
|
665
|
+
}
|
|
666
|
+
if (options.profile) {
|
|
667
|
+
assertProfileValid(options.profile);
|
|
668
|
+
process.env.AWS_PROFILE = options.profile;
|
|
669
|
+
}
|
|
670
|
+
awsAuthenticate(options);
|
|
671
|
+
const paths = getPaths(ifCore);
|
|
672
|
+
const { dryrun, tag } = options;
|
|
673
|
+
const configName = getWidgetUuid();
|
|
674
|
+
loadEnv(paths);
|
|
675
|
+
const { accountUuidProd, accountUuidStage } = loadAccountUuidsFromConfig(paths);
|
|
676
|
+
if (!accountUuidProd && !accountUuidStage) {
|
|
677
|
+
logTitle("No EPMS account UUIDs found in config.yml. Skipping EPMS update.");
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (dryrun) logTitle("🧪 DRY RUN MODE - No actual changes will be made 🧪");
|
|
681
|
+
const configuredVersion = resolveCoreVersion();
|
|
682
|
+
if (!configuredVersion) {
|
|
683
|
+
logTitle("⚠️ Could not determine the configured Tamaro version. Please ensure that the CORE_VERSION environment variable is set or that the CORE_URL environment variable contains a valid Tamaro version.");
|
|
684
|
+
logTitle("This may be expected behaviour if you are using a custom build of Tamaro Core. If so, you can safely ignore this warning. ⚠️");
|
|
685
|
+
}
|
|
686
|
+
const baseInfo = {
|
|
687
|
+
configured_version: configuredVersion ?? "unknown",
|
|
688
|
+
last_deployment: Math.trunc(Date.now() / 1e3),
|
|
689
|
+
read_only: false
|
|
690
|
+
};
|
|
691
|
+
const environments = [{
|
|
692
|
+
accountUuid: accountUuidStage,
|
|
693
|
+
env: "stage",
|
|
694
|
+
label: "Stage"
|
|
695
|
+
}, {
|
|
696
|
+
accountUuid: accountUuidProd,
|
|
697
|
+
env: "prod",
|
|
698
|
+
label: "Prod"
|
|
699
|
+
}];
|
|
700
|
+
for (const { accountUuid, env, label } of environments) {
|
|
701
|
+
if (!accountUuid) continue;
|
|
702
|
+
const epmsClient = epmsAuthenticate(env);
|
|
703
|
+
await assertUserIsSuperAdmin(epmsClient);
|
|
704
|
+
const organisationUuid = await epmsClient.getOrganisationIdByAccountUuid(accountUuid);
|
|
705
|
+
const body = {
|
|
706
|
+
...baseInfo,
|
|
707
|
+
account_uuid: accountUuid,
|
|
708
|
+
organisation_uuid: organisationUuid
|
|
709
|
+
};
|
|
710
|
+
logTitle(`\nUpdating EPMS ${label}:`);
|
|
711
|
+
logDataTable({
|
|
712
|
+
name: configName,
|
|
713
|
+
tag,
|
|
714
|
+
...body
|
|
715
|
+
});
|
|
716
|
+
if (!dryrun) {
|
|
717
|
+
await epmsClient.updateTamaro(configName, tag, body);
|
|
718
|
+
logSuccess(`✅ EPMS ${label} is updated.`);
|
|
719
|
+
} else logSuccess(`⏭️ EPMS ${label} would be updated (dry run).`);
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
const loadEnv = (paths) => {
|
|
723
|
+
const filePath = globSync(paths.appEnv).find((file) => path.basename(file) === ".env");
|
|
724
|
+
if (!filePath) halt("No \".env\" file found. Are you in the folder of the Tamaro configuration?");
|
|
725
|
+
config({
|
|
726
|
+
path: filePath,
|
|
727
|
+
quiet: true
|
|
728
|
+
});
|
|
729
|
+
};
|
|
730
|
+
const assertIsNotCore$3 = (ifCore) => {
|
|
731
|
+
if (ifCore()) halt("You cannot update EPMS in the context of Tamaro Core.");
|
|
732
|
+
};
|
|
733
|
+
//#endregion
|
|
327
734
|
//#region src/commands/deploy.ts
|
|
328
735
|
const deploy = async (options) => {
|
|
329
736
|
const { ifCore } = getIfCoreFns();
|
|
@@ -395,6 +802,8 @@ const deploy = async (options) => {
|
|
|
395
802
|
halt(error.stderr);
|
|
396
803
|
}
|
|
397
804
|
}
|
|
805
|
+
if (options.skipEpmsSyncOnlyUseInEmergenciesOrYouWillBeFired) logTitle("⚠️ EPMS sync skipped. You better know what you are doing.");
|
|
806
|
+
else await updateEpms(options);
|
|
398
807
|
const demoPage = `https://${AWS_S3_BUCKET_TAMARO}/${configName}/${tag}/index.html`;
|
|
399
808
|
const entryPoint = `https://${AWS_S3_BUCKET_TAMARO}/${configName}/${tag}/${entryFilename}`;
|
|
400
809
|
logTitle(`\nBundle for "${configName}" is deployed with tag "${tag}".`);
|
|
@@ -543,26 +952,32 @@ const listDeployed = async (options) => {
|
|
|
543
952
|
}
|
|
544
953
|
assertOptionsValid$4(options);
|
|
545
954
|
awsAuthenticate(options);
|
|
546
|
-
const
|
|
547
|
-
const { config } = options;
|
|
955
|
+
const { config, profile } = options;
|
|
548
956
|
const { ifCore } = getIfCoreFns();
|
|
549
957
|
const configName = config ?? ifCore("tamaro-core", getWidgetUuid());
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
out = runCommandSync(cmd).stdout;
|
|
557
|
-
} catch (error) {
|
|
558
|
-
out = error.stdout;
|
|
559
|
-
if (error.stderr) halt(error.stderr);
|
|
958
|
+
logTitle(!config && ifCore() ? `Listing deployments of Tamaro Core …` : `Listing deployments of "${configName}" customer configuration …`);
|
|
959
|
+
if (profile) process.env.AWS_PROFILE = profile;
|
|
960
|
+
const deployments = await getConfigDeployments(configName);
|
|
961
|
+
if (deployments.length === 0) {
|
|
962
|
+
console.log("\nNo deployments found.\n");
|
|
963
|
+
return;
|
|
560
964
|
}
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
965
|
+
const table = columnify(deployments.map((deployment, idx) => ({
|
|
966
|
+
"#": `${idx + 1}`,
|
|
967
|
+
"Deployed At": deployment.date,
|
|
968
|
+
Tag: deployment.tag,
|
|
969
|
+
Preview: createTerminalLink("🔍 Live Preview", deployment.fullUrl)
|
|
970
|
+
})), {
|
|
971
|
+
columnSplitter: " | ",
|
|
972
|
+
config: {
|
|
973
|
+
"#": { minWidth: 2 },
|
|
974
|
+
"Deployed At": { minWidth: 20 },
|
|
975
|
+
Preview: { minWidth: 16 },
|
|
976
|
+
Tag: { minWidth: 14 }
|
|
977
|
+
},
|
|
978
|
+
minWidth: 10
|
|
979
|
+
});
|
|
980
|
+
console.log(`\n${table}\n`);
|
|
566
981
|
};
|
|
567
982
|
const assertOptionsValid$4 = (options) => {
|
|
568
983
|
const { config, profile } = options;
|
|
@@ -574,10 +989,6 @@ const assertConfigValid = (config) => {
|
|
|
574
989
|
const regex = /^[\w-]+$/;
|
|
575
990
|
if (!regex.test(config)) halt(`Flag "--config" has forbidden format. Allowed format: ${regex.toString()}.`);
|
|
576
991
|
};
|
|
577
|
-
const parseTags = (lines) => {
|
|
578
|
-
const regex = /^\s*PRE\s*/;
|
|
579
|
-
return lines.filter((line) => regex.test(line)).map((line) => line.replace(regex, "").replace(/\/$/, ""));
|
|
580
|
-
};
|
|
581
992
|
//#endregion
|
|
582
993
|
//#region src/commands/serve.ts
|
|
583
994
|
const serve = async (options) => {
|
|
@@ -669,7 +1080,7 @@ const undeploy = async (options) => {
|
|
|
669
1080
|
const deployUrl = all ? `s3://${AWS_S3_BUCKET_TAMARO}/${configName}/` : `s3://${AWS_S3_BUCKET_TAMARO}/${configName}/${tag}/`;
|
|
670
1081
|
const description = all ? `all tags of "${configName}"` : `tag "${tag}" of "${configName}"`;
|
|
671
1082
|
if (dryRunFlag) logTitle("🧪 DRY RUN MODE - No actual changes will be made 🧪");
|
|
672
|
-
await promptConfirmation(`Are you sure you want to undeploy ${description}?`, options.
|
|
1083
|
+
await promptConfirmation(`Are you sure you want to undeploy ${description}?`, options.ci);
|
|
673
1084
|
const cmd = `
|
|
674
1085
|
aws s3 rm ${deployUrl}
|
|
675
1086
|
--recursive
|
|
@@ -698,6 +1109,8 @@ const undeploy = async (options) => {
|
|
|
698
1109
|
halt(error.stderr);
|
|
699
1110
|
}
|
|
700
1111
|
}
|
|
1112
|
+
if (!dryRunFlag && !all && !ifCore() && !options.skipEpmsSyncOnlyUseInEmergenciesOrYouWillBeFired) await archiveEpms(configName, tag, false);
|
|
1113
|
+
else if (all && !ifCore()) logTitle("⚠️ EPMS archive skipped: --all flag used. Archive entries in EPMS manually if needed.");
|
|
701
1114
|
logTitle(`\n${description} has been undeployed.`);
|
|
702
1115
|
logDataTable({ "Removed URL:": deployUrl });
|
|
703
1116
|
process.on("exit", () => {
|
|
@@ -707,6 +1120,33 @@ const undeploy = async (options) => {
|
|
|
707
1120
|
});
|
|
708
1121
|
});
|
|
709
1122
|
};
|
|
1123
|
+
const archiveEpms = async (configName, tag, dryrun) => {
|
|
1124
|
+
const { ifCore } = getIfCoreFns();
|
|
1125
|
+
const { accountUuidProd, accountUuidStage } = loadAccountUuidsFromConfig(getPaths(ifCore));
|
|
1126
|
+
if (!accountUuidProd && !accountUuidStage) {
|
|
1127
|
+
logTitle("No EPMS account UUIDs found in config.yml. Skipping EPMS archive.");
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
const environments = [{
|
|
1131
|
+
accountUuid: accountUuidStage,
|
|
1132
|
+
env: "stage",
|
|
1133
|
+
label: "Stage"
|
|
1134
|
+
}, {
|
|
1135
|
+
accountUuid: accountUuidProd,
|
|
1136
|
+
env: "prod",
|
|
1137
|
+
label: "Prod"
|
|
1138
|
+
}];
|
|
1139
|
+
for (const { accountUuid, env, label } of environments) {
|
|
1140
|
+
if (!accountUuid) continue;
|
|
1141
|
+
const epmsClient = epmsAuthenticate(env);
|
|
1142
|
+
await assertUserIsSuperAdmin(epmsClient);
|
|
1143
|
+
logTitle(`\nArchiving EPMS ${label}: ${configName}/${tag}`);
|
|
1144
|
+
if (!dryrun) {
|
|
1145
|
+
await epmsClient.archiveTamaro(configName, tag);
|
|
1146
|
+
logSuccess(`✅ EPMS ${label} entry archived.`);
|
|
1147
|
+
} else logSuccess(`⏭️ EPMS ${label} entry would be archived (dry run).`);
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
710
1150
|
const assertOptionsValid$1 = (options) => {
|
|
711
1151
|
const { all, profile, tag } = options;
|
|
712
1152
|
const { ifCore } = getIfCoreFns();
|
|
@@ -789,7 +1229,7 @@ const promptBucketTamaroEmailConfig = async () => {
|
|
|
789
1229
|
//#endregion
|
|
790
1230
|
//#region package.json
|
|
791
1231
|
var name = "@raisenow/tamaro-cli";
|
|
792
|
-
var version = "1.
|
|
1232
|
+
var version = "1.9.0-dev.2";
|
|
793
1233
|
//#endregion
|
|
794
1234
|
//#region src/cli.ts
|
|
795
1235
|
/**
|
|
@@ -807,7 +1247,7 @@ const cli = createCommand().name(name).version(version, "-v, --version");
|
|
|
807
1247
|
cli.command("dev").description("Run the local development server for Tamaro Core or a particular customer configuration").option("--local-core", "Load Tamaro Core from localhost:1234 instead of the CDN", false).option("--https", "Let local web server serve Tamaro with SSL encryption", false).option("--port <port>", "Web server port", `${DEFAULT_PORT}`).option("--env <env>", "Environment (dev, stage, prod)").option("--nolint", "Disable ESLint", false).option("--debug", "Enable debug mode", false).action(async (options) => {
|
|
808
1248
|
await dev(options);
|
|
809
1249
|
});
|
|
810
|
-
cli.command("build").description("Build an optimised (minified) bundle of Tamaro Core or a customer configuration").option("--local-core", "Load Tamaro Core from localhost:1234 instead of the CDN", false).option("--analyze", "Generate bundle statistics to \"reports\" folder", false).option("--serve", "Run the generated bundle with a local web server", false).option("--https", "Let local web server serve Tamaro with SSL encryption", false).option("--port <port>", "Web server port", `${DEFAULT_PORT}`).option("--env <env>", "Environment (dev, stage, prod)").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--deploy", "Deploy Tamaro Core or a customer configuration bundle to AWS S3", false).option("--tag <tag>", "Tag which should be used for the deployment of the bundle", DEFAULT_TAG).option("--nolint", "Disable ESLint", false).option("--debug", "Enable debug mode", false).action(async (options) => {
|
|
1250
|
+
cli.command("build").description("Build an optimised (minified) bundle of Tamaro Core or a customer configuration").option("--local-core", "Load Tamaro Core from localhost:1234 instead of the CDN", false).option("--analyze", "Generate bundle statistics to \"reports\" folder", false).option("--serve", "Run the generated bundle with a local web server", false).option("--https", "Let local web server serve Tamaro with SSL encryption", false).option("--port <port>", "Web server port", `${DEFAULT_PORT}`).option("--env <env>", "Environment (dev, stage, prod)").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--deploy", "Deploy Tamaro Core or a customer configuration bundle to AWS S3", false).option("--skip-epms-sync-only-use-in-emergencies-or-you-will-be-fired", "Skip the EPMS sync entirely. Only use this in emergencies.", false).option("--tag <tag>", "Tag which should be used for the deployment of the bundle", DEFAULT_TAG).option("--nolint", "Disable ESLint", false).option("--debug", "Enable debug mode", false).action(async (options) => {
|
|
811
1251
|
await build(options);
|
|
812
1252
|
if (options.serve) await serve(options);
|
|
813
1253
|
if (options.deploy) await deploy(options);
|
|
@@ -815,7 +1255,7 @@ cli.command("build").description("Build an optimised (minified) bundle of Tamaro
|
|
|
815
1255
|
cli.command("serve").description("Run a local web server for pre-built bundle").option("--port <port>", "Web server port", `${DEFAULT_PORT}`).option("--https", "Let local web server serve Tamaro with SSL encryption", false).action(async (options) => {
|
|
816
1256
|
await serve(options);
|
|
817
1257
|
});
|
|
818
|
-
cli.command("deploy").description("Deploy a pre-built Tamaro Core or customer configuration bundle to AWS S3").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--tag <tag>", "Tag which should be used for the deployment of the bundle", DEFAULT_TAG).option("--dryrun", "Displays the operations that would be performed without actually running them", false).action(async (options) => {
|
|
1258
|
+
cli.command("deploy").description("Deploy a pre-built Tamaro Core or customer configuration bundle to AWS S3").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--tag <tag>", "Tag which should be used for the deployment of the bundle", DEFAULT_TAG).option("--dryrun", "Displays the operations that would be performed without actually running them", false).option("--skip-epms-sync-only-use-in-emergencies-or-you-will-be-fired", "Skip the EPMS sync entirely. Only use this in emergencies.", false).action(async (options) => {
|
|
819
1259
|
await deploy(options);
|
|
820
1260
|
});
|
|
821
1261
|
cli.command("list-deployed").description("List deployments of Tamaro Core or a particular customer configuration").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--config <config>", "Configuration name (default: current customer configuration)").action(async (options) => {
|
|
@@ -824,7 +1264,7 @@ cli.command("list-deployed").description("List deployments of Tamaro Core or a p
|
|
|
824
1264
|
cli.command("deploy-email-config").description("Deploy a widget's email configuration to AWS S3").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--bucket <bucket>", "AWS bucket for email configs").option("--dryrun", "Displays the operations that would be performed without actually running them", false).action(async (options) => {
|
|
825
1265
|
await deployEmailConfig(options);
|
|
826
1266
|
});
|
|
827
|
-
cli.command("undeploy").description("Undeploy a Tamaro Core or customer configuration bundle from AWS S3").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--tag <tag>", "Tag which should be undeployed").option("--all", "Undeploy all tags", false).option("--dryrun", "Displays the operations that would be performed without actually running them", false).action(async (options) => {
|
|
1267
|
+
cli.command("undeploy").description("Undeploy a Tamaro Core or customer configuration bundle from AWS S3").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--tag <tag>", "Tag which should be undeployed").option("--all", "Undeploy all tags", false).option("--dryrun", "Displays the operations that would be performed without actually running them", false).option("--skip-epms-sync-only-use-in-emergencies-or-you-will-be-fired", "Skip EPMS archive sync (emergency use only)", false).action(async (options) => {
|
|
828
1268
|
await undeploy(options);
|
|
829
1269
|
});
|
|
830
1270
|
cli.command("undeploy-email-config").description("Undeploy a widget's email configuration from AWS S3").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--bucket <bucket>", "AWS bucket for email configs").option("--stage", "Undeploy from stage environment (alternative to --bucket)").option("--prod", "Undeploy from prod environment (alternative to --bucket)").option("--dryrun", "Displays the operations that would be performed without actually running them", false).action(async (options) => {
|
|
@@ -836,6 +1276,9 @@ cli.command("archive").description("Archive a customer configuration by moving i
|
|
|
836
1276
|
cli.command("unarchive").description("Unarchive a customer configuration by restoring it from the _archived folder").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--dryrun", "Displays the operations that would be performed without actually running them", false).action((options) => {
|
|
837
1277
|
unarchive(options);
|
|
838
1278
|
});
|
|
1279
|
+
cli.command("update-epms").description("Update EPMS with the currently deployed widget tags.").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--dryrun", "Displays the operations that would be performed without actually running them", false).option("--tag <tag>", "Tag which should be set for the deployment", DEFAULT_TAG).action(async (options) => {
|
|
1280
|
+
await updateEpms(options);
|
|
1281
|
+
});
|
|
839
1282
|
cli.command("validate").description("Validate the current Tamaro configuration to avoid common errors").action(async () => {
|
|
840
1283
|
await validate();
|
|
841
1284
|
});
|