contensis-cli 1.0.0-beta.8 → 1.0.0-beta.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +1146 -78
  2. package/dist/commands/connect.js +3 -3
  3. package/dist/commands/connect.js.map +2 -2
  4. package/dist/commands/create.js +45 -10
  5. package/dist/commands/create.js.map +2 -2
  6. package/dist/commands/diff.js +57 -0
  7. package/dist/commands/diff.js.map +7 -0
  8. package/dist/commands/execute.js +103 -0
  9. package/dist/commands/execute.js.map +7 -0
  10. package/dist/commands/get.js +107 -18
  11. package/dist/commands/get.js.map +2 -2
  12. package/dist/commands/globalOptions.js +22 -17
  13. package/dist/commands/globalOptions.js.map +2 -2
  14. package/dist/commands/import.js +46 -11
  15. package/dist/commands/import.js.map +2 -2
  16. package/dist/commands/index.js +16 -2
  17. package/dist/commands/index.js.map +2 -2
  18. package/dist/commands/list.js +53 -10
  19. package/dist/commands/list.js.map +2 -2
  20. package/dist/commands/login.js +3 -3
  21. package/dist/commands/login.js.map +2 -2
  22. package/dist/commands/push.js +9 -5
  23. package/dist/commands/push.js.map +2 -2
  24. package/dist/commands/remove.js +51 -8
  25. package/dist/commands/remove.js.map +2 -2
  26. package/dist/commands/set.js +139 -12
  27. package/dist/commands/set.js.map +2 -2
  28. package/dist/index.js +1 -1
  29. package/dist/index.js.map +2 -2
  30. package/dist/localisation/en-GB.js +193 -49
  31. package/dist/localisation/en-GB.js.map +2 -2
  32. package/dist/providers/CredentialProvider.js +36 -7
  33. package/dist/providers/CredentialProvider.js.map +3 -3
  34. package/dist/providers/SessionCacheProvider.js +21 -1
  35. package/dist/providers/SessionCacheProvider.js.map +2 -2
  36. package/dist/providers/file-provider.js +8 -4
  37. package/dist/providers/file-provider.js.map +3 -3
  38. package/dist/services/ContensisCliService.js +1092 -410
  39. package/dist/services/ContensisCliService.js.map +3 -3
  40. package/dist/shell.js +48 -14
  41. package/dist/shell.js.map +3 -3
  42. package/dist/util/console.printer.js +171 -55
  43. package/dist/util/console.printer.js.map +2 -2
  44. package/dist/util/diff.js +39 -0
  45. package/dist/util/diff.js.map +7 -0
  46. package/dist/util/index.js +8 -2
  47. package/dist/util/index.js.map +3 -3
  48. package/dist/util/logger.js +61 -29
  49. package/dist/util/logger.js.map +3 -3
  50. package/dist/util/timers.js +49 -0
  51. package/dist/util/timers.js.map +7 -0
  52. package/dist/version.js +1 -1
  53. package/dist/version.js.map +1 -1
  54. package/esbuild.config.js +3 -1
  55. package/package.json +2 -2
  56. package/src/commands/connect.ts +3 -2
  57. package/src/commands/create.ts +61 -8
  58. package/src/commands/diff.ts +41 -0
  59. package/src/commands/execute.ts +117 -0
  60. package/src/commands/get.ts +150 -14
  61. package/src/commands/globalOptions.ts +18 -17
  62. package/src/commands/import.ts +57 -7
  63. package/src/commands/index.ts +16 -1
  64. package/src/commands/list.ts +85 -11
  65. package/src/commands/login.ts +3 -2
  66. package/src/commands/push.ts +10 -3
  67. package/src/commands/remove.ts +66 -4
  68. package/src/commands/set.ts +189 -9
  69. package/src/index.ts +1 -4
  70. package/src/localisation/en-GB.ts +269 -66
  71. package/src/providers/CredentialProvider.ts +39 -6
  72. package/src/providers/SessionCacheProvider.ts +29 -2
  73. package/src/providers/file-provider.ts +12 -4
  74. package/src/services/ContensisCliService.ts +1384 -484
  75. package/src/shell.ts +52 -15
  76. package/src/util/console.printer.ts +240 -78
  77. package/src/util/diff.ts +17 -0
  78. package/src/util/index.ts +16 -7
  79. package/src/util/logger.ts +111 -31
  80. package/src/util/timers.ts +24 -0
  81. package/src/version.ts +1 -1
@@ -3,8 +3,16 @@ import path from 'path';
3
3
  import fetch from 'node-fetch';
4
4
  import inquirer from 'inquirer';
5
5
  import to from 'await-to-js';
6
- import { Component, ContentType } from 'contensis-core-api';
7
- import { isPassword, isSharedSecret, isUuid, url } from '~/util';
6
+ import chalk from 'chalk';
7
+ import { Component, ContentType, Project } from 'contensis-core-api';
8
+ import {
9
+ isPassword,
10
+ isSharedSecret,
11
+ isUuid,
12
+ tryParse,
13
+ tryStringify,
14
+ url,
15
+ } from '~/util';
8
16
  import SessionCacheProvider from '../providers/SessionCacheProvider';
9
17
  import ContensisAuthService from './ContensisAuthService';
10
18
  import CredentialProvider from '~/providers/CredentialProvider';
@@ -16,13 +24,24 @@ import {
16
24
  PushBlockParams,
17
25
  SourceCms,
18
26
  logEntriesTable,
27
+ ContentTypesResult,
28
+ Model,
29
+ MigrateModelsResult,
30
+ BlockActionType,
19
31
  } from 'migratortron';
20
- import { Entry } from 'contensis-management-api/lib/models';
32
+ import { Entry, Role } from 'contensis-management-api/lib/models';
21
33
 
22
34
  import { csvFormatter } from '~/util/csv.formatter';
23
35
  import { xmlFormatter } from '~/util/xml.formatter';
24
36
  import { jsonFormatter } from '~/util/json.formatter';
25
- import { printBlockVersion, printMigrateResult } from '~/util/console.printer';
37
+ import { diffLogStrings } from '~/util/diff';
38
+ import { promiseDelay } from '~/util/timers';
39
+ import {
40
+ printBlockVersion,
41
+ printMigrateResult,
42
+ printModelMigrationAnalysis,
43
+ printModelMigrationResult,
44
+ } from '~/util/console.printer';
26
45
  import { readJsonFile } from '~/providers/file-provider';
27
46
 
28
47
  type OutputFormat = 'json' | 'csv' | 'xml';
@@ -45,28 +64,31 @@ interface IAuthOptions {
45
64
  }
46
65
 
47
66
  interface IImportOptions {
48
- sourceEnv?: string;
67
+ sourceAlias?: string;
49
68
  sourceProjectId?: string;
50
69
  }
51
70
 
71
+ let insecurePasswordWarningShown = false;
72
+
52
73
  class ContensisCli {
53
74
  static quit = (error?: Error) => {
54
75
  process.removeAllListeners('exit');
55
76
  const exitCode = error ? 1 : 0;
56
77
 
57
- console.info(`\nExiting contensis-cli with exit code: ${exitCode}\n`);
78
+ // console.info(`\nExiting contensis-cli with exit code: ${exitCode}\n`);
58
79
  process.exit(exitCode);
59
80
  };
60
81
 
61
- cache: SessionCache;
82
+ private command: CliCommand;
83
+ private format?: OutputFormat;
84
+ private output?: string;
85
+ private session: SessionCacheProvider;
86
+
62
87
  contensis?: ContensisMigrationService;
63
88
  contensisOpts: Partial<MigrateRequest>;
64
- contentTypes?: ContentType[];
65
- components?: Component[];
66
- currentEnv: string;
67
89
  currentProject: string;
68
- env: EnvironmentCache;
69
- sourceEnv?: string;
90
+
91
+ sourceAlias?: string;
70
92
  targetEnv?: string;
71
93
  urls:
72
94
  | {
@@ -78,17 +100,42 @@ class ContensisCli {
78
100
  iisPreviewWeb: string;
79
101
  }
80
102
  | undefined;
81
- private command: CliCommand;
82
- private format?: OutputFormat;
83
- private output?: string;
84
103
  log = Logger;
85
104
  messages = LogMessages;
86
- private session: SessionCacheProvider;
87
105
 
88
106
  verb: string;
89
107
  noun: string;
90
108
  thirdArg: string;
91
109
 
110
+ get cache() {
111
+ return this.session.Get();
112
+ }
113
+
114
+ get currentEnv() {
115
+ return this.cache.currentEnvironment || '';
116
+ }
117
+
118
+ set currentEnv(currentEnvironment: string) {
119
+ this.session.Update({ currentEnvironment });
120
+ }
121
+
122
+ get env() {
123
+ const currentEnvironment = this.currentEnv;
124
+ const environments = this.cache.environments || {};
125
+
126
+ if (!currentEnvironment) return {} as EnvironmentCache;
127
+ else if (!!environments[currentEnvironment])
128
+ return environments[currentEnvironment];
129
+ else {
130
+ return {
131
+ history: [],
132
+ lastUserId: '',
133
+ projects: [],
134
+ versionStatus: 'latest',
135
+ } as EnvironmentCache;
136
+ }
137
+ }
138
+
92
139
  constructor(
93
140
  args: string[],
94
141
  outputOpts?: OutputOptions & IConnectOptions & IImportOptions,
@@ -106,40 +153,28 @@ class ContensisCli {
106
153
  }`.trim();
107
154
 
108
155
  this.session = new SessionCacheProvider();
109
- this.cache = this.session.Get();
156
+
110
157
  this.contensisOpts = contensisOpts;
111
158
  this.format = outputOpts?.format;
112
159
  this.output =
113
160
  outputOpts?.output && path.join(process.cwd(), outputOpts.output);
114
161
 
115
- const currentEnvironment =
116
- outputOpts?.alias || this.cache.currentEnvironment || '';
162
+ const currentEnvironment = outputOpts?.alias || this.currentEnv;
117
163
  const environments = this.cache.environments || {};
118
-
119
- if (!currentEnvironment) this.env = {} as EnvironmentCache;
120
- else if (!!environments[currentEnvironment])
121
- this.env = environments[currentEnvironment];
122
- else {
123
- this.env = {
124
- history: [],
125
- lastUserId: '',
126
- projects: [],
127
- versionStatus: 'latest',
128
- };
129
- }
164
+ this.currentEnv = currentEnvironment;
130
165
 
131
166
  const env = this.env;
132
167
 
133
168
  if (outputOpts?.projectId) env.currentProject = outputOpts.projectId;
134
169
  if (outputOpts?.user) env.lastUserId = outputOpts.user;
170
+ // setting this in env means passwordFallback is written to environments.json
135
171
  if (outputOpts?.password) env.passwordFallback = outputOpts.password;
136
172
  if (outputOpts?.clientId) env.lastUserId = outputOpts.clientId;
137
173
  if (outputOpts?.sharedSecret)
138
174
  env.passwordFallback = outputOpts.sharedSecret;
139
175
 
140
- this.currentEnv = currentEnvironment;
141
176
  this.currentProject = env?.currentProject || 'null';
142
- this.sourceEnv = outputOpts?.sourceEnv || currentEnvironment;
177
+ this.sourceAlias = outputOpts?.sourceAlias || currentEnvironment;
143
178
 
144
179
  if (currentEnvironment) {
145
180
  this.urls = url(currentEnvironment, env?.currentProject || 'website');
@@ -154,7 +189,7 @@ class ContensisCli {
154
189
  if (currentEnvironment) {
155
190
  env.history = [this.command];
156
191
  if (commandText) {
157
- environments[currentEnvironment] = this.env;
192
+ environments[currentEnvironment] = env;
158
193
  this.session.Update({
159
194
  currentEnvironment,
160
195
  environments,
@@ -181,45 +216,24 @@ class ContensisCli {
181
216
  };
182
217
 
183
218
  Connect = async (environment: string) => {
184
- const { cache, log, messages, session } = this;
219
+ const { log, messages, session } = this;
185
220
 
186
221
  if (environment) {
187
- const envCache = cache.environments[environment];
188
- if (!envCache)
189
- cache.environments[environment] = {
190
- versionStatus: 'published',
191
- history: [],
192
- lastUserId: '',
193
- projects: [],
194
- ...(!this.currentEnv ? this.env : {}),
195
- };
196
-
197
- this.env = cache.environments[environment];
198
222
  this.currentEnv = environment;
199
223
  this.urls = url(environment, 'website');
200
224
 
201
225
  const [fetchErr, response] = await to(fetch(this.urls.cms));
202
226
  if (response && response?.status < 400) {
203
227
  log.success(messages.connect.connected(environment));
228
+ session.UpdateEnv(this.env, environment);
204
229
 
205
230
  if (this.env?.lastUserId) {
206
- await this.ConnectContensis();
231
+ // await this.ConnectContensis();
207
232
  await this.PrintProjects();
208
233
  } else {
209
234
  log.warning(messages.projects.noList());
210
235
  log.help(messages.connect.tip());
211
- // cache.environments[environment] = {
212
- // versionStatus: 'published',
213
- // history: [],
214
- // lastUserId: '',
215
- // projects: [],
216
- // };
217
236
  }
218
-
219
- session.Update({
220
- currentEnvironment: environment,
221
- environments: cache.environments,
222
- });
223
237
  } else {
224
238
  // Cannot reach environment - status X
225
239
  log.error(
@@ -233,68 +247,75 @@ class ContensisCli {
233
247
  };
234
248
 
235
249
  ConnectContensis = async ({ commit = false } = {}) => {
236
- const { contensisOpts, currentEnv, env, log, messages } = this;
237
- const userId = env?.lastUserId;
238
- const isGuidId = userId && isUuid(userId);
250
+ if (!this.contensis) {
251
+ const { contensisOpts, currentEnv, env, log, messages } = this;
252
+ const userId = env?.lastUserId;
253
+ const isGuidId = userId && isUuid(userId);
239
254
 
240
- if (currentEnv && userId) {
241
- const [credentialError, credentials] = await new CredentialProvider(
242
- {
255
+ if (currentEnv && userId) {
256
+ const credentials = await this.GetCredentials(
243
257
  userId,
244
- alias: currentEnv,
245
- },
246
- env.passwordFallback
247
- ).Init();
258
+ env.passwordFallback
259
+ );
248
260
 
249
- if (credentialError && !credentials.current) {
250
- // Log problem with Credential Provider
251
- log.error(credentialError as any);
252
- return;
253
- }
254
- const cachedPassword = credentials?.current?.password;
255
-
256
- if (cachedPassword) {
257
- this.contensis = new ContensisMigrationService(
258
- {
259
- ...contensisOpts,
260
- source: {
261
- url: this.urls?.cms || '',
262
- username: !isGuidId ? userId : undefined,
263
- password: !isGuidId ? cachedPassword : undefined,
264
- clientId: isGuidId ? userId : undefined,
265
- sharedSecret: isGuidId ? cachedPassword : undefined,
266
- project: env?.currentProject || '',
267
- assetHostname: this.urls?.previewWeb,
261
+ const cachedPassword = credentials?.current?.password;
262
+
263
+ if (cachedPassword) {
264
+ this.contensis = new ContensisMigrationService(
265
+ {
266
+ ...contensisOpts,
267
+ source: {
268
+ url: this.urls?.cms || '',
269
+ username: !isGuidId ? userId : undefined,
270
+ password: !isGuidId ? cachedPassword : undefined,
271
+ clientId: isGuidId ? userId : undefined,
272
+ sharedSecret: isGuidId ? cachedPassword : undefined,
273
+ project: env?.currentProject || '',
274
+ assetHostname: this.urls?.previewWeb,
275
+ },
276
+ concurrency:
277
+ typeof contensisOpts.concurrency !== 'undefined'
278
+ ? contensisOpts.concurrency
279
+ : 3,
280
+ outputProgress: true,
268
281
  },
269
- concurrency:
270
- typeof contensisOpts.concurrency !== 'undefined'
271
- ? contensisOpts.concurrency
272
- : 3,
273
- outputProgress: true,
274
- },
275
- !commit
276
- );
282
+ !commit
283
+ );
284
+ }
285
+ } else {
286
+ if (!currentEnv) log.help(messages.connect.help());
287
+ if (!userId) log.help(messages.connect.tip());
277
288
  }
278
- } else {
279
- if (!currentEnv) log.help(messages.connect.help());
280
- if (!userId) log.help(messages.connect.tip());
281
289
  }
290
+ return this.contensis;
282
291
  };
283
292
 
284
293
  ConnectContensisImport = async ({
285
- commit,
286
- source,
287
- fileData,
288
- fileDataType,
294
+ commit = false,
295
+ fromFile,
296
+ importDataType,
289
297
  }: {
290
- commit: boolean;
291
- source: 'contensis' | 'file' | 'input';
292
- fileData?: any[] | string;
293
- fileDataType?: 'entries' | 'contentTypes' | 'components';
298
+ commit?: boolean;
299
+ fromFile?: string;
300
+ importDataType?:
301
+ | 'entries'
302
+ | 'contentTypes'
303
+ | 'components'
304
+ | 'models'
305
+ | 'user-input';
294
306
  }) => {
295
- const { contensisOpts, currentEnv, env, log, messages, sourceEnv } = this;
307
+ const source: 'contensis' | 'file' = fromFile ? 'file' : 'contensis';
308
+
309
+ const fileData = fromFile
310
+ ? readJsonFile<(Entry | ContentType | Component)[]>(fromFile) || []
311
+ : [];
312
+
313
+ if (typeof fileData === 'string')
314
+ throw new Error(`Import file format must be of type JSON`);
315
+
316
+ const { contensisOpts, currentEnv, env, log, messages, sourceAlias } = this;
296
317
  const environments = this.cache.environments || {};
297
- const sourceEnvironment = environments[sourceEnv || ''] || {};
318
+ const sourceEnvironment = environments[sourceAlias || ''] || {};
298
319
  const sourceCms =
299
320
  ('source' in contensisOpts && contensisOpts.source) ||
300
321
  ({} as Partial<SourceCms>);
@@ -303,7 +324,7 @@ class ContensisCli {
303
324
  const sourceProjectId =
304
325
  sourceCms.project || sourceEnvironment.currentProject || 'website';
305
326
  const isSourceGuidId = sourceUserId && isUuid(sourceUserId);
306
- const sourceUrls = url(sourceEnv || '', sourceProjectId);
327
+ const sourceUrls = url(sourceAlias || '', sourceProjectId);
307
328
 
308
329
  const sourcePassword =
309
330
  sourceCms.sharedSecret ||
@@ -314,40 +335,24 @@ class ContensisCli {
314
335
  const isTargetGuidId = targetUserId && isUuid(targetUserId);
315
336
 
316
337
  if (sourceUserId && currentEnv && targetUserId) {
317
- const [sourceCredentialError, sourceCredentials] =
318
- await new CredentialProvider(
319
- {
320
- userId: sourceUserId,
321
- alias: currentEnv,
322
- },
323
- sourcePassword
324
- ).Init();
325
-
326
- if (sourceCredentialError && !sourceCredentials.current) {
327
- // Log problem with Credential Provider
328
- logError(sourceCredentialError);
329
- return;
330
- }
338
+ const sourceCredentials = await this.GetCredentials(
339
+ sourceUserId,
340
+ sourcePassword,
341
+ sourceAlias,
342
+ false
343
+ );
344
+
331
345
  const cachedSourcePassword = sourceCredentials?.current?.password;
332
346
 
333
- const [targetCredentialError, targetCredentials] =
334
- await new CredentialProvider(
335
- {
336
- userId: targetUserId,
337
- alias: currentEnv,
338
- },
339
- env.passwordFallback
340
- ).Init();
347
+ const targetCredentials = await this.GetCredentials(
348
+ targetUserId,
349
+ env.passwordFallback
350
+ );
341
351
 
342
- if (targetCredentialError && !targetCredentials.current) {
343
- // Log problem with Credential Provider
344
- log.error(targetCredentialError as any);
345
- return;
346
- }
347
352
  const cachedTargetPassword = targetCredentials?.current?.password;
348
353
 
349
354
  if (cachedSourcePassword && cachedTargetPassword) {
350
- if (source === 'file' || source === 'input') {
355
+ if (source === 'file' || importDataType === 'user-input') {
351
356
  this.contensis = new ContensisMigrationService(
352
357
  {
353
358
  concurrency: 3,
@@ -362,12 +367,11 @@ class ContensisCli {
362
367
  targetProjects: [env.currentProject || ''],
363
368
  assetHostname: this.urls?.previewWeb,
364
369
  },
365
- ...(fileDataType ? { [fileDataType]: fileData } : {}),
370
+ ...(importDataType ? { [importDataType]: fileData } : {}),
366
371
  },
367
372
  !commit
368
373
  );
369
- }
370
- if (source === 'contensis') {
374
+ } else if (source === 'contensis') {
371
375
  this.contensis = new ContensisMigrationService(
372
376
  {
373
377
  concurrency: 3,
@@ -400,6 +404,40 @@ class ContensisCli {
400
404
  if (!currentEnv) log.help(messages.connect.help());
401
405
  if (!targetUserId) log.help(messages.connect.tip());
402
406
  }
407
+ return this.contensis;
408
+ };
409
+
410
+ GetCredentials = async (
411
+ userId: string,
412
+ password?: string,
413
+ currentEnv = this.currentEnv,
414
+ saveCurrentEnv = true
415
+ ): Promise<CredentialProvider | undefined> => {
416
+ const { log, messages } = this;
417
+ if (userId) {
418
+ const [credentialError, credentials] = await new CredentialProvider(
419
+ { userId, alias: currentEnv },
420
+ password
421
+ ).Init();
422
+
423
+ if (credentialError && !credentials.current) {
424
+ // Log problem with Credential Provider
425
+ log.error(credentialError as any);
426
+ return;
427
+ }
428
+
429
+ if (credentials.remarks.secure !== true) {
430
+ if (!insecurePasswordWarningShown) {
431
+ log.warning(messages.login.insecurePassword());
432
+ insecurePasswordWarningShown = true;
433
+ }
434
+ } else {
435
+ const env = this.cache.environments[currentEnv];
436
+ env.passwordFallback = undefined;
437
+ this.session.UpdateEnv(env, currentEnv, saveCurrentEnv);
438
+ }
439
+ return credentials;
440
+ }
403
441
  };
404
442
 
405
443
  Login = async (
@@ -409,111 +447,102 @@ class ContensisCli {
409
447
  promptPassword = true,
410
448
  sharedSecret = isSharedSecret(this.env.passwordFallback),
411
449
  silent = false,
450
+ attempt = 1,
412
451
  }: {
413
452
  password?: string;
414
453
  promptPassword?: boolean;
415
454
  sharedSecret?: string;
416
455
  silent?: boolean;
417
- }
456
+ attempt?: number;
457
+ } = {}
418
458
  ): Promise<string | undefined> => {
419
- let inputPassword = password;
459
+ let inputPassword = password || sharedSecret;
420
460
  const { log, messages } = this;
421
461
 
422
462
  if (userId) {
423
- const { cache, currentEnv, env } = this;
463
+ const { currentEnv, env } = this;
424
464
 
425
465
  if (currentEnv) {
426
- const [credentialError, credentials] = await new CredentialProvider(
427
- { userId, alias: currentEnv },
428
- inputPassword || sharedSecret
429
- ).Init();
430
-
431
- if (credentialError && !credentials.current) {
432
- // Log problem with Credential Provider
433
- log.error(credentialError as any);
434
- return;
435
- }
436
-
437
- if (credentials.remarks.secure !== true)
438
- log.warning(messages.login.insecurePassword());
439
-
440
- const cachedPassword = isPassword(credentials?.current?.password);
441
- const cachedSecret = isSharedSecret(credentials?.current?.password);
442
-
443
- if (
444
- !sharedSecret &&
445
- !inputPassword &&
446
- !cachedPassword &&
447
- !cachedSecret &&
448
- promptPassword
449
- ) {
450
- // Password prompt
451
- ({ inputPassword } = await inquirer.prompt([
452
- {
453
- type: 'password',
454
- message: messages.login.passwordPrompt(currentEnv, userId),
455
- name: 'inputPassword',
456
- mask: '*',
457
- prefix: undefined,
458
- },
459
- ]));
460
- }
466
+ const credentials = await this.GetCredentials(userId, inputPassword);
467
+
468
+ if (credentials) {
469
+ const cachedPassword = isPassword(credentials.current?.password);
470
+ const cachedSecret = isSharedSecret(credentials.current?.password);
471
+
472
+ if (!cachedPassword && !cachedSecret && promptPassword) {
473
+ // Password prompt
474
+ ({ inputPassword } = await inquirer.prompt([
475
+ {
476
+ type: 'password',
477
+ message: messages.login.passwordPrompt(currentEnv, userId),
478
+ name: 'inputPassword',
479
+ mask: '*',
480
+ prefix: undefined,
481
+ },
482
+ ]));
483
+ }
461
484
 
462
- if (sharedSecret || cachedSecret || inputPassword || cachedPassword) {
463
- const authService = new ContensisAuthService({
464
- username: userId,
465
- password: inputPassword || cachedPassword,
466
- projectId: env?.currentProject || 'website',
467
- rootUrl: this.urls?.cms || '',
468
- clientId: userId,
469
- clientSecret: sharedSecret || cachedSecret,
470
- });
485
+ if (inputPassword || cachedPassword || cachedSecret) {
486
+ const authService = new ContensisAuthService({
487
+ username: userId,
488
+ password: inputPassword || cachedPassword,
489
+ projectId: env?.currentProject || 'website',
490
+ rootUrl: this.urls?.cms || '',
491
+ clientId: userId,
492
+ clientSecret: sharedSecret || cachedSecret,
493
+ });
494
+
495
+ const [authError, bearerToken] = await to(
496
+ authService.BearerToken()
497
+ );
471
498
 
472
- const [authError, bearerToken] = await to(authService.BearerToken());
473
-
474
- // Login successful
475
- if (bearerToken) {
476
- // Set env vars
477
- env.authToken = bearerToken;
478
- env.lastUserId = userId;
479
- env.passwordFallback =
480
- credentials.remarks.secure !== true
481
- ? credentials.current?.password
482
- : undefined;
483
-
484
- if (!silent) {
485
- Logger.success(messages.login.success(currentEnv, userId));
486
- await this.PrintProjects();
487
- }
488
- if (inputPassword) await credentials.Save(inputPassword);
489
- if (sharedSecret) await credentials.Save(sharedSecret);
490
- } else if (authError) {
491
- Logger.error(authError.toString());
492
- // Clear env vars
493
- env.authToken = '';
494
- env.lastUserId = '';
495
- env.passwordFallback = undefined;
496
-
497
- // If the auth error was raised using a cached password
498
- if (
499
- (cachedPassword || cachedSecret) &&
500
- credentials.remarks.secure
501
- ) {
502
- // Remove any bad stored credential and trigger login prompt again
503
- await credentials.Delete();
504
- return await this.Login(userId, { password, sharedSecret });
505
- } else {
506
- throw new Error(messages.login.failed(currentEnv, userId));
499
+ // Login successful
500
+ if (bearerToken) {
501
+ // Set env vars
502
+ env.authToken = bearerToken;
503
+ env.lastUserId = userId;
504
+ env.passwordFallback =
505
+ credentials.remarks.secure !== true
506
+ ? credentials.current?.password
507
+ : undefined;
508
+
509
+ // Persist env before finding projects or doing anything else
510
+ this.session.UpdateEnv(env);
511
+ if (inputPassword) await credentials.Save(inputPassword);
512
+ if (sharedSecret) await credentials.Save(sharedSecret);
513
+
514
+ if (!silent) {
515
+ Logger.success(messages.login.success(currentEnv, userId));
516
+ await this.PrintProjects();
517
+ }
518
+ } else if (authError) {
519
+ Logger.error(authError.toString());
520
+ // Clear env vars
521
+ env.authToken = '';
522
+ env.lastUserId = '';
523
+ env.passwordFallback = undefined;
524
+ // Persist env to remove cleared values
525
+ this.session.UpdateEnv(env);
526
+
527
+ // If the auth error was raised using a cached password
528
+ if (
529
+ (cachedPassword || cachedSecret) &&
530
+ credentials.remarks.secure
531
+ ) {
532
+ // Remove any bad stored credential and trigger login prompt again
533
+ await credentials.Delete();
534
+ return await this.Login(userId, { password, sharedSecret });
535
+ } else {
536
+ throw new Error(messages.login.failed(currentEnv, userId));
537
+ }
507
538
  }
508
- }
509
539
 
510
- // Persist env
511
- this.session.Update({
512
- environments: cache.environments,
513
- });
514
- return env.authToken;
515
- } else {
516
- Logger.error(messages.login.passwordPrompt(currentEnv, userId));
540
+ return env.authToken;
541
+ } else {
542
+ Logger.error(messages.login.passwordPrompt());
543
+ if (attempt < 2)
544
+ return await this.Login(userId, { attempt: attempt + 1 });
545
+ }
517
546
  }
518
547
  } else {
519
548
  // No environment set, use `contensis connect {alias}` first
@@ -525,19 +554,65 @@ class ContensisCli {
525
554
  }
526
555
  };
527
556
 
557
+ PrintContensisVersion = async () => {
558
+ const { log, messages } = this;
559
+ const contensis = await this.ConnectContensis();
560
+
561
+ if (contensis) {
562
+ // Retrieve projects list for env
563
+ const [projectsErr, projects] = await to(
564
+ contensis.projects.GetSourceProjects()
565
+ );
566
+
567
+ if (Array.isArray(projects)) {
568
+ // Print contensis version to console
569
+ this.HandleFormattingAndOutput(contensis.contensisVersion, () =>
570
+ log.raw(log.highlightText(contensis.contensisVersion))
571
+ );
572
+ }
573
+
574
+ if (projectsErr) {
575
+ log.error(messages.projects.noList());
576
+ log.error(projectsErr.message);
577
+ }
578
+ }
579
+ };
580
+
581
+ PrintBearerToken = async () => {
582
+ const { log, messages } = this;
583
+ const contensis = await this.ConnectContensis();
584
+
585
+ if (contensis) {
586
+ // Retrieve token for env
587
+ const [error, token] = await to(
588
+ contensis.content.sourceRepo.repo.BearerToken()
589
+ );
590
+ if (token) {
591
+ // Print bearer token to console
592
+ this.HandleFormattingAndOutput(token, () =>
593
+ log.raw(log.highlightText(token))
594
+ );
595
+ }
596
+
597
+ if (error) {
598
+ log.error(messages.projects.noList());
599
+ log.error(error.message);
600
+ }
601
+ }
602
+ };
603
+
528
604
  PrintProjects = async () => {
529
- const { cache, currentEnv, currentProject, log, messages, session } = this;
530
- if (!this.contensis) await this.ConnectContensis();
605
+ const { currentProject, log, messages, session } = this;
606
+ const contensis = await this.ConnectContensis();
531
607
 
532
- if (this.contensis) {
608
+ if (contensis) {
533
609
  // Retrieve projects list for env
534
610
  const [projectsErr, projects] = await to(
535
- this.contensis.projects.GetSourceProjects()
611
+ contensis.projects.GetSourceProjects()
536
612
  );
537
613
 
538
614
  if (Array.isArray(projects)) {
539
615
  // save these projects in cache
540
- const currentVals = cache.environments[currentEnv] || {};
541
616
  const nextCurrentProject =
542
617
  currentProject && currentProject !== 'null'
543
618
  ? currentProject
@@ -545,46 +620,80 @@ class ContensisCli {
545
620
  ? 'website'
546
621
  : undefined;
547
622
 
548
- cache.environments[currentEnv] = {
549
- ...currentVals,
623
+ session.UpdateEnv({
550
624
  projects: projects.map(p => p.id),
551
625
  currentProject: nextCurrentProject,
552
- };
626
+ });
553
627
 
554
628
  log.success(messages.projects.list());
629
+ log.raw('');
630
+
555
631
  this.HandleFormattingAndOutput(projects, () => {
556
632
  // print the projects to console
557
- for (const project of projects) {
633
+ for (const project of projects.sort((a, b) =>
634
+ a.id.localeCompare(b.id)
635
+ )) {
636
+ let color;
637
+ try {
638
+ color = chalk.keyword((project as any).color);
639
+ } catch (ex) {
640
+ color = chalk.white;
641
+ }
558
642
  console.log(
559
- ` - ${nextCurrentProject === project.id ? '* ' : ''}[${
560
- project.primaryLanguage
561
- }] ${project.id}`
643
+ `${
644
+ nextCurrentProject === project.id
645
+ ? `>> ${log.boldText(color(project.id))}`
646
+ : ` ${color(project.id)}`
647
+ } ${log.infoText(
648
+ `[${project.supportedLanguages
649
+ .map(l =>
650
+ l === project.primaryLanguage ? `*${log.boldText(l)}` : l
651
+ )
652
+ .join(' ')}]`
653
+ )}`
562
654
  );
563
655
  }
564
656
  });
565
657
 
566
- session.Update({
567
- environments: cache.environments,
568
- });
658
+ if (!this.SetProject(nextCurrentProject))
659
+ log.warning(messages.projects.tip());
660
+ }
569
661
 
570
- if (nextCurrentProject) {
571
- this.env = cache.environments[currentEnv];
572
- this.SetProject(nextCurrentProject);
573
- }
662
+ if (projectsErr) {
663
+ log.error(messages.projects.noList());
664
+ log.error(projectsErr.message);
665
+ }
666
+ }
667
+ };
668
+
669
+ PrintProject = async (projectId = this.currentProject) => {
670
+ const { log, messages, session } = this;
671
+ const contensis = await this.ConnectContensis();
672
+
673
+ if (contensis) {
674
+ // Retrieve projects list for env
675
+ const [projectsErr, projects] = await to(
676
+ contensis.projects.GetSourceProjects()
677
+ );
678
+
679
+ const foundProject = projects?.find(
680
+ p => p.id.toLowerCase() === projectId.toLowerCase()
681
+ );
682
+
683
+ if (foundProject) {
684
+ log.raw('');
685
+ this.HandleFormattingAndOutput(foundProject, log.object);
574
686
  }
575
687
 
576
688
  if (projectsErr) {
577
689
  log.error(messages.projects.noList());
578
690
  log.error(projectsErr.message);
579
691
  }
580
- // } else {
581
- // log.warning(messages.projects.noList());
582
- // log.help(messages.connect.tip());
583
692
  }
584
693
  };
585
694
 
586
- SetProject = async (projectId = '') => {
587
- const { cache, env, log, messages, session } = this;
695
+ SetProject = (projectId = 'website') => {
696
+ const { env, log, messages, session } = this;
588
697
  let nextProjectId: string | undefined;
589
698
  if (env?.projects.length > 0 && env?.lastUserId) {
590
699
  nextProjectId = env.projects.find(
@@ -592,10 +701,9 @@ class ContensisCli {
592
701
  );
593
702
  if (nextProjectId) {
594
703
  env.currentProject = nextProjectId;
595
- session.Update({
596
- environments: cache.environments,
597
- });
704
+ session.UpdateEnv(env);
598
705
  log.success(messages.projects.set(projectId));
706
+ log.raw('');
599
707
  } else {
600
708
  log.error(messages.projects.failedSet(projectId));
601
709
  }
@@ -607,8 +715,8 @@ class ContensisCli {
607
715
  return nextProjectId;
608
716
  };
609
717
 
610
- SetVersion = async (versionStatus: 'latest' | 'published') => {
611
- const { cache, env, log, messages, session } = this;
718
+ SetVersion = (versionStatus: 'latest' | 'published') => {
719
+ const { env, log, messages, session } = this;
612
720
  if (!['latest', 'published'].includes(versionStatus)) {
613
721
  log.error(messages.version.invalid(versionStatus));
614
722
  return false;
@@ -618,10 +726,7 @@ class ContensisCli {
618
726
  return false;
619
727
  }
620
728
  if (env?.projects.length > 0 && env?.lastUserId) {
621
- env.versionStatus = versionStatus;
622
- session.Update({
623
- environments: cache.environments,
624
- });
729
+ session.UpdateEnv({ versionStatus });
625
730
  log.success(messages.version.set(this.currentEnv, versionStatus));
626
731
  return true;
627
732
  } else {
@@ -632,30 +737,13 @@ class ContensisCli {
632
737
  }
633
738
  };
634
739
 
635
- HydrateContensis = async () => {
636
- const { log } = this;
637
- if (!this.contensis) await this.ConnectContensis();
638
-
639
- if (this.contensis) {
640
- // Retrieve content types list for env
641
- const [contensisErr, models] = await to(
642
- this.contensis.models.HydrateContensisRepositories()
643
- );
644
-
645
- if (contensisErr) {
646
- log.error(contensisErr.message);
647
- return contensisErr;
648
- }
649
- }
650
- };
651
-
652
740
  PrintApiKeys = async () => {
653
741
  const { currentEnv, log, messages } = this;
654
- if (!this.contensis) await this.ConnectContensis();
742
+ const contensis = await this.ConnectContensis();
655
743
 
656
- if (this.contensis) {
744
+ if (contensis) {
657
745
  // Retrieve keys list for env
658
- const [keysErr, apiKeys] = await this.contensis.apiKeys.GetKeys();
746
+ const [keysErr, apiKeys] = await contensis.apiKeys.GetKeys();
659
747
 
660
748
  if (Array.isArray(apiKeys)) {
661
749
  log.success(messages.keys.list(currentEnv));
@@ -689,70 +777,492 @@ class ContensisCli {
689
777
 
690
778
  CreateApiKey = async (name: string, description?: string) => {
691
779
  const { currentEnv, log, messages } = this;
692
- if (!this.contensis) await this.ConnectContensis();
780
+ const contensis = await this.ConnectContensis();
693
781
 
694
- if (this.contensis) {
695
- const [err, key] = await this.contensis.apiKeys.CreateKey(
696
- name,
697
- description
698
- );
782
+ if (contensis) {
783
+ const [err, key] = await contensis.apiKeys.CreateKey(name, description);
699
784
 
700
785
  if (key) {
701
786
  log.success(messages.keys.created(currentEnv, name));
702
787
 
703
788
  // print the key details to console
704
789
  console.log(
705
- ` - ${key.name}${
706
- key.description ? ` (${key.description})` : ''
707
- } [${key.dateModified.toString().substring(0, 10)} ${key.modifiedBy}]`
790
+ ` - ${chalk.bold(key.name)} [${key.dateModified
791
+ .toString()
792
+ .substring(0, 10)} ${key.modifiedBy}]`
793
+ );
794
+ if (key.description)
795
+ console.log(` ${log.infoText(key.description)}`);
796
+ console.log(` ${chalk.bold.grey`id`}: ${key.id}`);
797
+ console.log(
798
+ ` ${chalk.bold.grey`sharedSecret`}: ${key.sharedSecret}`
708
799
  );
709
- console.log(` - id: ${key.id}`);
710
- console.log(` - sharedSecret: ${key.sharedSecret}`);
800
+ console.log('');
801
+ log.help(messages.keys.tip());
802
+ }
803
+
804
+ if (err) {
805
+ log.error(messages.keys.failedCreate(currentEnv, name), err);
806
+ }
807
+ }
808
+ };
809
+
810
+ RemoveApiKey = async (id: string) => {
811
+ const { currentEnv, log, messages } = this;
812
+ const contensis = await this.ConnectContensis({ commit: true });
813
+
814
+ if (contensis) {
815
+ const [err, key] = await contensis.apiKeys.RemoveKey(id);
816
+
817
+ if (!err) {
818
+ log.success(messages.keys.removed(currentEnv, id));
819
+ console.log('');
820
+ } else {
821
+ log.error(messages.keys.failedRemove(currentEnv, id), err);
822
+ }
823
+ }
824
+ };
825
+
826
+ PrintRoles = async () => {
827
+ const { currentEnv, log, messages } = this;
828
+ const contensis = await this.ConnectContensis();
829
+
830
+ if (contensis) {
831
+ // Retrieve roles list for env
832
+ const [rolesErr, roles] = await to(contensis.roles.GetRoles());
833
+
834
+ if (Array.isArray(roles)) {
835
+ log.success(messages.roles.list(currentEnv));
836
+
837
+ if (!roles.length) log.help(messages.roles.noneExist());
838
+
839
+ this.HandleFormattingAndOutput(roles, () => {
840
+ // print the roles to console
841
+ for (const {
842
+ id,
843
+ name,
844
+ description,
845
+ enabled,
846
+ assignments,
847
+ permissions,
848
+ } of roles) {
849
+ const color = enabled ? (s: string) => s : log.infoText;
850
+
851
+ console.log(color(` - ${chalk.bold(name)} ${log.infoText(id)}`));
852
+ if (description) console.log(log.infoText(` ${description}`));
853
+ if (enabled === false)
854
+ console.log(` ${chalk.bold.grey('enabled')}: false`);
855
+ if (assignments.groups?.length)
856
+ console.log(
857
+ ` ${chalk.bold.grey('groups')}: ${assignments.groups.join(
858
+ ', '
859
+ )}`
860
+ );
861
+ if (assignments.users?.length)
862
+ console.log(
863
+ ` ${chalk.bold.grey('users')}: ${assignments.users.join(
864
+ ', '
865
+ )}`
866
+ );
867
+ if (assignments.apiKeys?.length)
868
+ console.log(
869
+ ` ${chalk.bold.grey('keys')}: ${assignments.apiKeys.join(
870
+ ', '
871
+ )}`
872
+ );
873
+
874
+ if (permissions.entries?.length) {
875
+ console.log(` ${chalk.bold.grey('entries')}:`);
876
+ for (const p of permissions.entries)
877
+ console.log(
878
+ ` ${p.id}: ${log.infoText(
879
+ p.actions.length > 2
880
+ ? p.actions.length
881
+ : p.actions.join(', ')
882
+ )}`
883
+ );
884
+ }
885
+ if (permissions.contentTypes?.length)
886
+ console.log(
887
+ ` ${chalk.bold.grey(
888
+ 'contentTypes'
889
+ )}: ${permissions.contentTypes
890
+ .map(
891
+ p =>
892
+ `${p.id} [${p.actions.join(',')}] ${p.languages.join(
893
+ ' '
894
+ )}`
895
+ )
896
+ .join(', ')}`
897
+ );
898
+ }
899
+ });
900
+ }
901
+
902
+ if (rolesErr) {
903
+ log.error(messages.roles.noList(currentEnv));
904
+ log.error(jsonFormatter(rolesErr));
905
+ }
906
+ }
907
+ };
908
+
909
+ PrintRole = async (roleNameOrId: string) => {
910
+ const { currentEnv, log, messages } = this;
911
+ const contensis = await this.ConnectContensis();
912
+
913
+ if (contensis) {
914
+ // Retrieve roles list for env
915
+ const [rolesErr, roles] = await to(contensis.roles.GetRoles());
916
+
917
+ if (Array.isArray(roles)) {
918
+ log.success(messages.roles.list(currentEnv));
919
+
920
+ const role =
921
+ roles.find(
922
+ r =>
923
+ r.id === roleNameOrId ||
924
+ r.name.toLowerCase() === roleNameOrId.toLowerCase()
925
+ ) ||
926
+ roles.find(r =>
927
+ r.name.toLowerCase().includes(roleNameOrId.toLowerCase())
928
+ );
929
+
930
+ if (role) this.HandleFormattingAndOutput(role, log.object);
931
+ else log.error(messages.roles.failedGet(currentEnv, roleNameOrId));
932
+ }
933
+
934
+ if (rolesErr) {
935
+ log.error(messages.roles.noList(currentEnv));
936
+ log.error(jsonFormatter(rolesErr));
937
+ }
938
+ }
939
+ };
940
+
941
+ CreateRole = async (role: Partial<Role>) => {
942
+ const { currentEnv, log, messages } = this;
943
+ const contensis = await this.ConnectContensis();
944
+
945
+ if (contensis) {
946
+ const [err, created] = await contensis.roles.CreateRole(role as Role);
947
+
948
+ if (created) {
949
+ log.success(
950
+ messages.roles.created(currentEnv, role.id || role.name || '')
951
+ );
952
+
953
+ this.HandleFormattingAndOutput(created, log.object);
954
+
955
+ log.help(messages.roles.tip());
956
+ return role.id;
957
+ }
958
+
959
+ if (err) {
960
+ log.error(
961
+ messages.roles.failedCreate(currentEnv, role.id || role.name || ''),
962
+ err
963
+ );
964
+ }
965
+ }
966
+ };
967
+
968
+ UpdateRole = async (roleNameOrId: string, role: Partial<Role>) => {
969
+ const { currentEnv, log, messages } = this;
970
+ const contensis = await this.ConnectContensis();
971
+
972
+ if (contensis) {
973
+ // Retrieve roles list for env
974
+ const [rolesErr, roles] = await to(contensis.roles.GetRoles());
975
+
976
+ if (Array.isArray(roles)) {
977
+ log.success(messages.roles.list(currentEnv));
978
+
979
+ const existingRole = roles.find(
980
+ r =>
981
+ r.id === roleNameOrId ||
982
+ r.name.toLowerCase() === roleNameOrId.toLowerCase()
983
+ );
984
+ if (existingRole) {
985
+ log.info(messages.roles.setPayload());
986
+ log.object(role);
987
+ log.raw(``);
988
+ const [updateErr, updated] = await contensis.roles.UpdateRole(
989
+ existingRole.id,
990
+ role
991
+ );
992
+ if (updateErr)
993
+ log.error(messages.roles.failedSet(currentEnv, roleNameOrId));
994
+ else {
995
+ log.success(messages.roles.set());
996
+
997
+ this.HandleFormattingAndOutput(updated, log.object);
998
+ }
999
+ } else {
1000
+ // Role does not exist
1001
+ log.error(messages.roles.failedGet(currentEnv, roleNameOrId));
1002
+ }
1003
+ }
1004
+
1005
+ if (rolesErr) {
1006
+ log.error(messages.roles.noList(currentEnv));
1007
+ log.error(jsonFormatter(rolesErr));
1008
+ }
1009
+ }
1010
+ };
1011
+
1012
+ RemoveRole = async (roleNameOrId: string) => {
1013
+ const { currentEnv, log, messages } = this;
1014
+ const contensis = await this.ConnectContensis();
1015
+
1016
+ if (contensis) {
1017
+ // Retrieve roles list for env
1018
+ const [rolesErr, roles] = await to(contensis.roles.GetRoles());
1019
+
1020
+ if (Array.isArray(roles)) {
1021
+ log.success(messages.roles.list(currentEnv));
1022
+
1023
+ const existingRole = roles.find(
1024
+ r =>
1025
+ r.id === roleNameOrId ||
1026
+ r.name.toLowerCase() === roleNameOrId.toLowerCase()
1027
+ );
1028
+ if (existingRole) {
1029
+ const [deleteErr] = await contensis.roles.RemoveRole(existingRole.id);
1030
+
1031
+ if (deleteErr)
1032
+ log.error(messages.roles.failedRemove(currentEnv, roleNameOrId));
1033
+ else log.success(messages.roles.removed(currentEnv, roleNameOrId));
1034
+ } else {
1035
+ // Role does not exist
1036
+ log.error(messages.roles.failedGet(currentEnv, roleNameOrId));
1037
+ }
1038
+ }
1039
+
1040
+ if (rolesErr) {
1041
+ log.error(messages.roles.noList(currentEnv));
1042
+ log.error(jsonFormatter(rolesErr));
1043
+ }
1044
+ }
1045
+ };
1046
+
1047
+ CreateProject = async (project: Project) => {
1048
+ const { currentEnv, log, messages } = this;
1049
+ const contensis = await this.ConnectContensis();
1050
+
1051
+ if (contensis) {
1052
+ const [err, created] = await contensis.projects.CreateProject(project);
1053
+
1054
+ if (created) {
1055
+ log.success(messages.projects.created(currentEnv, project.id));
1056
+
1057
+ this.HandleFormattingAndOutput(created, () => {
1058
+ // set the CLI project to the newly created project
1059
+ this.SetProject(project.id);
1060
+ // print all the projects to console
1061
+ this.PrintProjects();
1062
+ });
1063
+ return project.id;
1064
+ }
1065
+
1066
+ if (err) {
1067
+ log.error(messages.projects.failedCreate(currentEnv, project.id), err);
1068
+ }
1069
+ }
1070
+ };
1071
+
1072
+ UpdateProject = async (project: Partial<Project>) => {
1073
+ const { currentEnv, currentProject, log, messages } = this;
1074
+ const contensis = await this.ConnectContensis();
1075
+
1076
+ if (contensis) {
1077
+ const [err, updated] = await contensis.projects.UpdateProject({
1078
+ id: currentProject,
1079
+ ...project,
1080
+ });
1081
+
1082
+ if (updated) {
1083
+ log.success(messages.projects.updated(currentEnv, currentProject));
1084
+
1085
+ this.HandleFormattingAndOutput(updated, log.object);
1086
+ return updated.id;
711
1087
  }
712
- console.log('');
713
1088
 
714
1089
  if (err) {
715
- log.error(messages.keys.failedCreate(currentEnv, name), err);
1090
+ log.error(
1091
+ messages.projects.failedUpdate(currentEnv, currentProject),
1092
+ err
1093
+ );
716
1094
  }
717
1095
  }
718
1096
  };
719
1097
 
720
- RemoveApiKey = async (id: string) => {
721
- const { currentEnv, log, messages } = this;
722
- if (!this.contensis) await this.ConnectContensis({ commit: true });
723
-
724
- if (this.contensis) {
725
- const [err, key] = await this.contensis.apiKeys.RemoveKey(id);
1098
+ PrintContentModels = async (modelIds: string[] = []) => {
1099
+ const { currentProject, log, messages } = this;
1100
+ const contensis = await this.ConnectContensis();
1101
+ if (contensis) {
1102
+ // Retrieve models list for env
1103
+ const models = await contensis.models.contentModels();
1104
+ const contentTypes = await contensis.models.contentTypes();
1105
+ const components = await contensis.models.components();
1106
+
1107
+ // Models to output to console
1108
+ const returnModels = modelIds?.length
1109
+ ? models?.filter((m: Model) =>
1110
+ modelIds.some(id => id.toLowerCase() === m.id.toLowerCase())
1111
+ )
1112
+ : undefined;
1113
+
1114
+ // Generate a list of contentTypeIds and componentIds from all models
1115
+ // and dependencies
1116
+ const contentTypeIds = Array.from(
1117
+ new Set([
1118
+ ...(returnModels || models || []).map(m => m.id),
1119
+ ...(returnModels || models || [])
1120
+ .map(m => m.dependencies?.contentTypes?.map(c => c[0]) || [])
1121
+ .flat(),
1122
+ ])
1123
+ );
1124
+ const componentIds = Array.from(
1125
+ new Set(
1126
+ (returnModels || models || [])
1127
+ .map(m => m.dependencies?.components?.map(c => c[0]) || [])
1128
+ .flat()
1129
+ )
1130
+ );
726
1131
 
727
- if (!err) {
728
- log.success(messages.keys.removed(currentEnv, id));
729
- console.log('');
1132
+ // Create an array of all the content types and component definitions
1133
+ // we will use this when outputting to a file
1134
+ const contentModelBackup = [
1135
+ ...contentTypes.filter(c =>
1136
+ contentTypeIds.map(i => i.toLowerCase()).includes(c.id.toLowerCase())
1137
+ ),
1138
+ ...components.filter(c =>
1139
+ componentIds.map(i => i.toLowerCase()).includes(c.id.toLowerCase())
1140
+ ),
1141
+ ];
1142
+
1143
+ if (Array.isArray(returnModels)) {
1144
+ log.success(messages.models.list(currentProject));
1145
+ this.HandleFormattingAndOutput(contentModelBackup, () => {
1146
+ // print the content models to console
1147
+ for (const model of returnModels) {
1148
+ log.raw('');
1149
+ log.object(model);
1150
+ }
1151
+ log.raw('');
1152
+ });
730
1153
  } else {
731
- log.error(messages.keys.failedRemove(currentEnv, id), err);
1154
+ log.success(
1155
+ messages.models.get(currentProject, models?.length.toString() || '0')
1156
+ );
1157
+ log.raw('');
1158
+ if (models?.length) {
1159
+ this.HandleFormattingAndOutput(contentModelBackup, () => {
1160
+ // print the content models to console
1161
+ for (const model of models) {
1162
+ const components = model.components?.length || 0;
1163
+ const contentTypes = model.contentTypes?.length || 0;
1164
+ const dependencies =
1165
+ (model.dependencies?.components?.length || 0) +
1166
+ (model.dependencies?.contentTypes?.length || 0);
1167
+ const dependencyOf =
1168
+ (model.dependencyOf?.components?.length || 0) +
1169
+ (model.dependencyOf?.contentTypes?.length || 0);
1170
+
1171
+ const hasAny =
1172
+ components + contentTypes + dependencies + dependencyOf;
1173
+ log.raw(
1174
+ ` - ${log.highlightText(log.boldText(model.id))} ${
1175
+ hasAny
1176
+ ? log.infoText(
1177
+ `{ ${components ? `components: ${components}, ` : ''}${
1178
+ contentTypes ? `contentTypes: ${contentTypes}, ` : ''
1179
+ }${
1180
+ dependencies ? `references: ${dependencies}, ` : ''
1181
+ }${
1182
+ dependencyOf ? `required by: ${dependencyOf}` : ''
1183
+ } }`
1184
+ )
1185
+ : ''
1186
+ }`
1187
+ );
1188
+ }
1189
+ log.raw('');
1190
+ });
1191
+ }
732
1192
  }
733
1193
  }
734
1194
  };
735
1195
 
736
- GetContentTypes = async () => {
1196
+ ImportContentModels = async ({
1197
+ commit,
1198
+ fromFile,
1199
+ }: {
1200
+ commit: boolean;
1201
+ fromFile: string;
1202
+ }) => {
737
1203
  const { currentProject, log, messages } = this;
738
- let err;
739
- if (!this.contensis) err = await this.HydrateContensis();
740
1204
 
741
- if (err) log.error(messages.contenttypes.noList(currentProject));
742
- if (this.contensis) {
743
- this.contentTypes = this.contensis.models.contentTypes();
744
- this.components = this.contensis.models.components();
1205
+ const fileData = fromFile
1206
+ ? readJsonFile<(ContentType | Component)[]>(fromFile) || []
1207
+ : [];
1208
+ if (typeof fileData === 'string')
1209
+ throw new Error(`Import file format must be of type JSON`);
1210
+
1211
+ const contensis = await this.ConnectContensisImport({
1212
+ commit,
1213
+ fromFile,
1214
+ importDataType: 'models',
1215
+ });
1216
+
1217
+ if (contensis) {
1218
+ log.line();
1219
+ if (contensis.isPreview) {
1220
+ console.log(log.successText(` -- IMPORT PREVIEW -- `));
1221
+ } else {
1222
+ console.log(log.warningText(` *** COMMITTING IMPORT *** `));
1223
+ }
1224
+
1225
+ const [migrateErr, result] = await contensis.MigrateContentModels();
1226
+
1227
+ if (migrateErr) logError(migrateErr);
1228
+ else
1229
+ this.HandleFormattingAndOutput(result, () => {
1230
+ // print the results to console
1231
+ if (!commit) {
1232
+ log.raw(log.boldText(`\nContent types:`));
1233
+ if (!result.contentTypes) log.info(`- None returned\n`);
1234
+ else printModelMigrationAnalysis(this, result.contentTypes);
1235
+
1236
+ log.raw(log.boldText(`\nComponents:`));
1237
+ if (!result.components) log.info(`- None returned\n`);
1238
+ else printModelMigrationAnalysis(this, result.components);
1239
+ } else {
1240
+ const migrateResult = result as MigrateModelsResult;
1241
+ log.raw(log.boldText(`\nContent types:`));
1242
+ printModelMigrationResult(
1243
+ this,
1244
+ migrateResult[currentProject].contentTypes
1245
+ );
1246
+
1247
+ log.raw(log.boldText(`\nComponents:`));
1248
+ printModelMigrationResult(
1249
+ this,
1250
+ migrateResult[currentProject].components
1251
+ );
1252
+ }
1253
+ });
745
1254
  } else {
746
- log.warning(messages.contenttypes.noList(currentProject));
1255
+ log.warning(messages.models.noList(currentProject));
1256
+ log.help(messages.connect.tip());
747
1257
  }
748
1258
  };
749
1259
 
750
1260
  PrintContentTypes = async () => {
751
1261
  const { currentProject, log, messages } = this;
752
- await this.GetContentTypes();
753
- if (this.contensis) {
1262
+ const contensis = await this.ConnectContensis();
1263
+ if (contensis) {
754
1264
  // Retrieve content types list for env
755
- const { contentTypes } = this;
1265
+ const contentTypes = await contensis.models.contentTypes();
756
1266
 
757
1267
  if (Array.isArray(contentTypes)) {
758
1268
  log.success(messages.contenttypes.list(currentProject));
@@ -773,10 +1283,10 @@ class ContensisCli {
773
1283
 
774
1284
  PrintContentType = async (contentTypeId: string) => {
775
1285
  const { currentProject, log, messages } = this;
776
- await this.GetContentTypes();
777
- if (this.contensis) {
1286
+ const contensis = await this.ConnectContensis();
1287
+ if (contensis) {
778
1288
  // Retrieve content types list for env
779
- const { contentTypes } = this;
1289
+ const contentTypes = await contensis.models.contentTypes();
780
1290
 
781
1291
  if (Array.isArray(contentTypes)) {
782
1292
  const contentType = contentTypes.find(
@@ -799,15 +1309,12 @@ class ContensisCli {
799
1309
 
800
1310
  RemoveContentTypes = async (contentTypeIds: string[], commit = false) => {
801
1311
  const { currentProject, log, messages } = this;
802
- if (!this.contensis)
803
- await this.ConnectContensisImport({
804
- source: 'input',
805
- commit,
806
- });
807
- if (this.contensis) {
808
- const [err, result] = await this.contensis.DeleteContentTypes(
809
- contentTypeIds
810
- );
1312
+ const contensis = await this.ConnectContensisImport({
1313
+ commit,
1314
+ importDataType: 'user-input',
1315
+ });
1316
+ if (contensis) {
1317
+ const [err, result] = await contensis.DeleteContentTypes(contentTypeIds);
811
1318
 
812
1319
  if (err) {
813
1320
  log.error(
@@ -822,7 +1329,7 @@ class ContensisCli {
822
1329
  messages.contenttypes.removed(
823
1330
  currentProject,
824
1331
  contentTypeIds.join('", "'),
825
- !this.contensis.isPreview
1332
+ !contensis.isPreview
826
1333
  )
827
1334
  );
828
1335
  // print the results to console
@@ -851,22 +1358,21 @@ class ContensisCli {
851
1358
 
852
1359
  if (!Array.isArray(fileData)) fileData = [fileData];
853
1360
 
854
- await this.ConnectContensisImport({
1361
+ const contensis = await this.ConnectContensisImport({
855
1362
  commit,
856
- source: fromFile ? 'file' : 'contensis',
1363
+ importDataType: fromFile ? 'user-input' : undefined,
857
1364
  });
858
1365
 
859
- if (this.contensis) {
1366
+ if (contensis) {
860
1367
  // Pass each content type to the target repo
861
1368
  for (const contentType of fileData) {
862
1369
  // Fix invalid data
863
1370
  contentType.projectId = currentProject;
864
1371
  delete contentType.uuid;
865
1372
 
866
- const [err, created, createStatus] =
867
- await this.contensis.models.targetRepos[
868
- currentProject
869
- ].repo.UpsertContentType(false, contentType);
1373
+ const [err, created, createStatus] = await contensis.models.targetRepos[
1374
+ currentProject
1375
+ ].repo.UpsertContentType(false, contentType);
870
1376
 
871
1377
  if (err) log.error(err.message, err);
872
1378
  if (createStatus) {
@@ -884,12 +1390,59 @@ class ContensisCli {
884
1390
  }
885
1391
  };
886
1392
 
1393
+ DiffModels = async (
1394
+ {
1395
+ fromFile,
1396
+ }: {
1397
+ fromFile: string;
1398
+ },
1399
+ modelIds: string[] = []
1400
+ ) => {
1401
+ const { log } = this;
1402
+
1403
+ let fileData = fromFile ? readJsonFile<ContentType[]>(fromFile) || [] : [];
1404
+ if (typeof fileData === 'string')
1405
+ throw new Error(`Import file format must be of type JSON`);
1406
+
1407
+ if (!Array.isArray(fileData)) fileData = [fileData];
1408
+
1409
+ const contensis = await this.ConnectContensisImport({
1410
+ fromFile,
1411
+ importDataType: 'models',
1412
+ });
1413
+
1414
+ if (contensis) {
1415
+ const [err, result] = (await to(
1416
+ contensis.models.Diff(fileData.length ? fileData : modelIds)
1417
+ )) as [Error | null, ContentTypesResult | undefined];
1418
+
1419
+ if (err) log.error(err.message, err);
1420
+ if (result)
1421
+ // print the content type to console
1422
+ this.HandleFormattingAndOutput(result, () => {
1423
+ log.success(
1424
+ `Queried models ${log.infoText(
1425
+ `"${result.query.modelIds?.join(', ')}"`
1426
+ )}\n`
1427
+ );
1428
+
1429
+ log.raw(log.boldText(`Content types:`));
1430
+ if (!result.contentTypes) log.info(`- None returned\n`);
1431
+ else printModelMigrationAnalysis(this, result.contentTypes);
1432
+
1433
+ log.raw(log.boldText(`Components:`));
1434
+ if (!result.components) log.info(`- None returned\n`);
1435
+ else printModelMigrationAnalysis(this, result.components);
1436
+ });
1437
+ }
1438
+ };
1439
+
887
1440
  PrintComponents = async () => {
888
1441
  const { currentProject, log, messages } = this;
889
- await this.GetContentTypes();
890
- if (this.contensis) {
1442
+ const contensis = await this.ConnectContensis();
1443
+ if (contensis) {
891
1444
  // Retrieve components list for env
892
- const { components } = this;
1445
+ const components = await contensis.models.components();
893
1446
 
894
1447
  if (Array.isArray(components)) {
895
1448
  log.success(messages.components.list(currentProject));
@@ -911,10 +1464,10 @@ class ContensisCli {
911
1464
 
912
1465
  PrintComponent = async (componentId: string) => {
913
1466
  const { currentProject, log, messages } = this;
914
- await this.GetContentTypes();
915
- if (this.contensis) {
1467
+ const contensis = await this.ConnectContensis();
1468
+ if (contensis) {
916
1469
  // Retrieve content types list for env
917
- const { components } = this;
1470
+ const components = await contensis.models.components();
918
1471
 
919
1472
  if (Array.isArray(components)) {
920
1473
  const component = components.find(
@@ -933,13 +1486,12 @@ class ContensisCli {
933
1486
 
934
1487
  RemoveComponents = async (componentIds: string[], commit = false) => {
935
1488
  const { currentProject, log, messages } = this;
936
- if (!this.contensis)
937
- await this.ConnectContensisImport({
938
- source: 'input',
939
- commit,
940
- });
941
- if (this.contensis) {
942
- const [err, result] = await this.contensis.DeleteContentTypes(
1489
+ const contensis = await this.ConnectContensisImport({
1490
+ commit,
1491
+ importDataType: 'user-input',
1492
+ });
1493
+ if (contensis) {
1494
+ const [err, result] = await contensis.DeleteContentTypes(
943
1495
  undefined,
944
1496
  componentIds
945
1497
  );
@@ -957,7 +1509,7 @@ class ContensisCli {
957
1509
  messages.components.removed(
958
1510
  currentProject,
959
1511
  componentIds.join('", "'),
960
- !this.contensis.isPreview
1512
+ !contensis.isPreview
961
1513
  )
962
1514
  );
963
1515
  // print the results to console
@@ -986,22 +1538,21 @@ class ContensisCli {
986
1538
 
987
1539
  if (!Array.isArray(fileData)) fileData = [fileData];
988
1540
 
989
- await this.ConnectContensisImport({
1541
+ const contensis = await this.ConnectContensisImport({
990
1542
  commit,
991
- source: fromFile ? 'file' : 'contensis',
1543
+ importDataType: fromFile ? 'user-input' : undefined,
992
1544
  });
993
1545
 
994
- if (this.contensis) {
1546
+ if (contensis) {
995
1547
  // Pass each component to the target repo
996
1548
  for (const component of fileData) {
997
1549
  // Fix invalid data
998
1550
  component.projectId = currentProject;
999
1551
  delete component.uuid;
1000
1552
 
1001
- const [err, created, createStatus] =
1002
- await this.contensis.models.targetRepos[
1003
- currentProject
1004
- ].repo.UpsertComponent(false, component);
1553
+ const [err, created, createStatus] = await contensis.models.targetRepos[
1554
+ currentProject
1555
+ ].repo.UpsertComponent(false, component);
1005
1556
 
1006
1557
  if (err) log.error(err.message, err);
1007
1558
  if (createStatus) {
@@ -1019,38 +1570,42 @@ class ContensisCli {
1019
1570
  }
1020
1571
  };
1021
1572
 
1022
- RemoveEntry = async (id: string, commit = false) => {
1023
- const { currentEnv, log, messages } = this;
1024
- if (!this.contensis)
1025
- await this.ConnectContensisImport({
1026
- source: 'input',
1027
- commit,
1028
- });
1573
+ RemoveEntries = async (commit = false) => {
1574
+ const { currentEnv, currentProject, log, messages } = this;
1575
+ const contensis = await this.ConnectContensisImport({
1576
+ commit,
1577
+ importDataType: 'user-input',
1578
+ });
1029
1579
 
1030
- if (this.contensis) {
1031
- if (this.contensis.isPreview) {
1580
+ if (contensis) {
1581
+ if (contensis.isPreview) {
1032
1582
  console.log(log.successText(` -- PREVIEW -- `));
1033
1583
  } else {
1034
1584
  console.log(log.warningText(` *** COMMITTING DELETE *** `));
1035
1585
  }
1036
- const [err, result] = await this.contensis.DeleteEntries();
1586
+ const [err, result] = await contensis.DeleteEntries();
1037
1587
  if (result)
1038
1588
  this.HandleFormattingAndOutput(result, () => {
1039
1589
  // print the migrateResult to console
1040
- printMigrateResult(this, result, { action: 'delete' });
1590
+ printMigrateResult(this, result, {
1591
+ action: 'delete',
1592
+ showAllEntries: true,
1593
+ });
1041
1594
  });
1042
1595
  if (
1043
1596
  !err &&
1044
- ((!commit &&
1045
- Object.values(result.entriesToMigrate)?.[0].totalCount > 0) ||
1597
+ ((!commit && result.entriesToMigrate[currentProject].totalCount) ||
1046
1598
  (commit && result.migrateResult?.deleted))
1047
1599
  ) {
1048
- log.success(messages.entries.removed(currentEnv, id, commit));
1049
- if (!commit) log.help(messages.entries.commitTip());
1600
+ log.success(messages.entries.removed(currentEnv, commit));
1601
+ if (!commit) {
1602
+ log.raw(``);
1603
+ log.help(messages.entries.commitTip());
1604
+ }
1050
1605
  } else {
1051
- log.error(messages.entries.failedRemove(currentEnv, id), err);
1052
- if (!Object.values(result.entriesToMigrate)?.[0].totalCount)
1053
- log.help(messages.entries.notFound(id));
1606
+ log.error(messages.entries.failedRemove(currentEnv), err);
1607
+ if (!result.entriesToMigrate[currentProject].totalCount)
1608
+ log.help(messages.entries.notFound(currentEnv));
1054
1609
  }
1055
1610
  }
1056
1611
  };
@@ -1061,21 +1616,21 @@ class ContensisCli {
1061
1616
  withDependents?: boolean;
1062
1617
  }) => {
1063
1618
  const { currentProject, log, messages } = this;
1064
- await this.ConnectContensis();
1619
+ const contensis = await this.ConnectContensis();
1065
1620
 
1066
- if (this.contensis) {
1621
+ if (contensis) {
1067
1622
  log.line();
1068
- const entries = await this.contensis.GetEntries({ withDependents });
1623
+ const entries = await contensis.GetEntries({ withDependents });
1069
1624
  this.HandleFormattingAndOutput(entries, () =>
1070
1625
  // print the entries to console
1071
1626
  logEntriesTable(
1072
1627
  entries,
1073
1628
  currentProject,
1074
- this.contensis?.payload.query?.fields
1629
+ contensis.payload.query?.fields
1075
1630
  )
1076
1631
  );
1077
1632
  } else {
1078
- log.warning(messages.contenttypes.noList(currentProject));
1633
+ log.warning(messages.models.noList(currentProject));
1079
1634
  log.help(messages.connect.tip());
1080
1635
  }
1081
1636
  };
@@ -1083,89 +1638,157 @@ class ContensisCli {
1083
1638
  ImportEntries = async ({
1084
1639
  commit,
1085
1640
  fromFile,
1641
+ logOutput,
1086
1642
  }: {
1087
1643
  commit: boolean;
1088
1644
  fromFile: string;
1645
+ logOutput: string;
1089
1646
  }) => {
1090
- const { currentProject, log, messages } = this;
1091
-
1092
- const fileData = fromFile ? readJsonFile<Entry[]>(fromFile) || [] : [];
1093
- if (typeof fileData === 'string')
1094
- throw new Error(`Import file format must be of type JSON`);
1647
+ const { currentEnv, currentProject, log, messages } = this;
1095
1648
 
1096
- await this.ConnectContensisImport({
1649
+ const contensis = await this.ConnectContensisImport({
1097
1650
  commit,
1098
- source: fromFile ? 'file' : 'contensis',
1099
- fileData,
1100
- fileDataType: 'entries',
1651
+ fromFile,
1652
+ importDataType: 'entries',
1101
1653
  });
1102
1654
 
1103
- if (this.contensis) {
1655
+ if (contensis) {
1104
1656
  log.line();
1105
- if (this.contensis.isPreview) {
1657
+ if (contensis.isPreview) {
1106
1658
  console.log(log.successText(` -- IMPORT PREVIEW -- `));
1107
1659
  } else {
1108
1660
  console.log(log.warningText(` *** COMMITTING IMPORT *** `));
1109
1661
  }
1110
1662
 
1111
- const [migrateErr, migrateResult] = await this.contensis.MigrateEntries();
1663
+ const [err, result] = await contensis.MigrateEntries();
1112
1664
 
1113
- if (migrateErr) logError(migrateErr);
1665
+ if (err) logError(err);
1114
1666
  else
1115
- this.HandleFormattingAndOutput(migrateResult, () => {
1667
+ this.HandleFormattingAndOutput(result, () => {
1116
1668
  // print the migrateResult to console
1117
- printMigrateResult(this, migrateResult);
1669
+ printMigrateResult(this, result, {
1670
+ showAllEntries: logOutput === 'all',
1671
+ showChangedEntries: logOutput === 'changes',
1672
+ });
1118
1673
  });
1674
+
1675
+ if (
1676
+ !err &&
1677
+ !result.errors?.length &&
1678
+ ((!commit && result.entriesToMigrate[currentProject].totalCount) ||
1679
+ (commit &&
1680
+ (result.migrateResult?.created || result.migrateResult?.updated)))
1681
+ ) {
1682
+ log.success(
1683
+ messages.entries.imported(
1684
+ currentEnv,
1685
+ commit,
1686
+ commit
1687
+ ? (result.migrateResult?.created || 0) +
1688
+ (result.migrateResult?.updated || 0)
1689
+ : result.entriesToMigrate[currentProject].totalCount
1690
+ )
1691
+ );
1692
+ if (!commit) {
1693
+ log.raw(``);
1694
+ log.help(messages.entries.commitTip());
1695
+ }
1696
+ } else {
1697
+ log.error(messages.entries.failedImport(currentEnv), err);
1698
+ if (!result.entriesToMigrate[currentProject].totalCount)
1699
+ log.help(messages.entries.notFound(currentEnv));
1700
+ }
1119
1701
  } else {
1120
- log.warning(messages.contenttypes.noList(currentProject));
1702
+ log.warning(messages.models.noList(currentProject));
1121
1703
  log.help(messages.connect.tip());
1122
1704
  }
1123
1705
  };
1124
1706
 
1125
- PrintWebhookSubscriptions = async (
1126
- subscriptionIds?: string[],
1127
- name?: string
1128
- ) => {
1707
+ PrintWebhookSubscriptions = async (subscriptionIdsOrNames?: string[]) => {
1129
1708
  const { currentEnv, log, messages } = this;
1130
- if (!this.contensis) await this.ConnectContensis();
1131
- if (this.contensis) {
1709
+ const contensis = await this.ConnectContensis();
1710
+ if (contensis) {
1132
1711
  // Retrieve webhooks list for env
1133
1712
  const [webhooksErr, webhooks] =
1134
- await this.contensis.subscriptions.webhooks.GetSubscriptions();
1135
-
1136
- const filteredResults =
1137
- typeof name === 'string'
1138
- ? webhooks.filter(w =>
1139
- w.name?.toLowerCase().includes(name.toLowerCase())
1140
- )
1141
- : Array.isArray(subscriptionIds)
1142
- ? webhooks.filter(w => subscriptionIds?.some(id => id === w.id))
1143
- : webhooks;
1713
+ await contensis.subscriptions.webhooks.GetSubscriptions();
1714
+
1715
+ const filteredResults = subscriptionIdsOrNames?.length
1716
+ ? webhooks?.filter(
1717
+ w =>
1718
+ subscriptionIdsOrNames?.some(idname =>
1719
+ w.name?.toLowerCase().includes(idname.toLowerCase())
1720
+ ) ||
1721
+ subscriptionIdsOrNames?.some(
1722
+ id => id.toLowerCase() === w.id.toLowerCase()
1723
+ )
1724
+ )
1725
+ : webhooks;
1144
1726
 
1145
1727
  if (Array.isArray(filteredResults)) {
1146
- this.HandleFormattingAndOutput(filteredResults, () => {
1147
- // print the keys to console
1148
- log.success(messages.webhooks.list(currentEnv));
1149
- for (const {
1150
- id,
1151
- description,
1152
- method,
1153
- name,
1154
- version,
1155
- url,
1156
- } of filteredResults) {
1157
- console.log(
1158
- ` - ${name}${
1159
- description ? ` (${description})` : ''
1160
- } [${version.modified.toString().substring(0, 10)} ${
1161
- version.modifiedBy
1162
- }]`
1163
- );
1164
- console.log(` ${id}`);
1165
- console.log(` [${method}] ${url}`);
1166
- }
1167
- console.log('');
1168
- });
1728
+ log.success(messages.webhooks.list(currentEnv));
1729
+ if (!webhooks?.length) log.warning(messages.webhooks.noneExist());
1730
+ else {
1731
+ this.HandleFormattingAndOutput(filteredResults, () => {
1732
+ // print the keys to console
1733
+ for (const {
1734
+ id,
1735
+ description,
1736
+ method,
1737
+ name,
1738
+ version,
1739
+ url,
1740
+ enabled,
1741
+ topics,
1742
+ templates,
1743
+ headers,
1744
+ } of filteredResults) {
1745
+ console.log(
1746
+ log.infoText(
1747
+ ` ${chalk.bold.white`- ${name}`} ${id} [${(
1748
+ version.modified || version.created
1749
+ )
1750
+ .toString()
1751
+ .substring(0, 10)} ${
1752
+ version.modifiedBy || version.createdBy
1753
+ }]`
1754
+ )
1755
+ );
1756
+ if (description) console.log(log.infoText` ${description}`);
1757
+ console.log(` ${log.infoText`[${method}]`} ${url}`);
1758
+ if (headers && Object.keys(headers).length) {
1759
+ console.log(` ${log.infoText`headers`}:`);
1760
+
1761
+ for (const [key, { value, secret }] of Object.entries(headers))
1762
+ console.log(
1763
+ ` ${chalk.bold.gray(key)}: ${secret ? '🤐' : value}`
1764
+ );
1765
+ }
1766
+ if (topics?.length)
1767
+ if (topics?.length === 1)
1768
+ console.log(
1769
+ ` ${log.infoText`topics`}: ${topics
1770
+ .map(t => JSON.stringify(t))
1771
+ .join(' ')
1772
+ .replaceAll('"', '')
1773
+ .replaceAll(',', ' ')
1774
+ .replaceAll('{', '')
1775
+ .replaceAll('}', '')}`
1776
+ );
1777
+ else {
1778
+ console.log(` ${log.infoText`topics`}:`);
1779
+ log.objectRecurse(topics, 1, ' ');
1780
+ }
1781
+ if (templates && Object.keys(templates).length)
1782
+ console.log(
1783
+ ` ${log.infoText`templates`}: ${Object.keys(
1784
+ templates
1785
+ ).join(' ')}`
1786
+ );
1787
+ if (enabled === false)
1788
+ console.log(` ${log.infoText`enabled`}: ${enabled}`);
1789
+ }
1790
+ });
1791
+ }
1169
1792
  }
1170
1793
 
1171
1794
  if (webhooksErr) {
@@ -1176,16 +1799,16 @@ class ContensisCli {
1176
1799
  };
1177
1800
 
1178
1801
  PrintBlocks = async () => {
1179
- const { currentEnv, log, messages } = this;
1180
- if (!this.contensis) await this.ConnectContensis();
1181
- if (this.contensis) {
1802
+ const { currentEnv, env, log, messages } = this;
1803
+ const contensis = await this.ConnectContensis();
1804
+ if (contensis) {
1182
1805
  // Retrieve blocks list for env
1183
- const [err, blocks] = await this.contensis.blocks.GetBlocks();
1806
+ const [err, blocks] = await contensis.blocks.GetBlocks();
1184
1807
 
1185
1808
  if (Array.isArray(blocks)) {
1186
1809
  this.HandleFormattingAndOutput(blocks, () => {
1187
1810
  // print the blocks to console
1188
- log.success(messages.blocks.list(currentEnv));
1811
+ log.success(messages.blocks.list(currentEnv, env.currentProject));
1189
1812
  for (const {
1190
1813
  id,
1191
1814
  description,
@@ -1226,10 +1849,10 @@ class ContensisCli {
1226
1849
  version: string
1227
1850
  ) => {
1228
1851
  const { currentEnv, env, log, messages } = this;
1229
- if (!this.contensis) await this.ConnectContensis();
1230
- if (this.contensis) {
1852
+ const contensis = await this.ConnectContensis();
1853
+ if (contensis) {
1231
1854
  // Retrieve block version
1232
- const [err, blocks] = await this.contensis.blocks.GetBlockVersions(
1855
+ const [err, blocks] = await contensis.blocks.GetBlockVersions(
1233
1856
  blockId,
1234
1857
  branch,
1235
1858
  version
@@ -1239,7 +1862,7 @@ class ContensisCli {
1239
1862
  this.HandleFormattingAndOutput(blocks, () => {
1240
1863
  // print the version detail to console
1241
1864
  log.success(
1242
- messages.blocks.get(`${currentEnv}:${env.currentProject}`)
1865
+ messages.blocks.get(blockId, currentEnv, env.currentProject)
1243
1866
  );
1244
1867
  for (const block of blocks)
1245
1868
  printBlockVersion(
@@ -1268,18 +1891,20 @@ class ContensisCli {
1268
1891
  const { currentEnv, env, log, messages } = this;
1269
1892
 
1270
1893
  // Output request to console
1271
- messages.blocks.tryPush(
1272
- block.id,
1273
- block.source.branch,
1274
- currentEnv,
1275
- env.currentProject
1894
+ log.info(
1895
+ messages.blocks.tryPush(
1896
+ block.id,
1897
+ block.source.branch,
1898
+ currentEnv,
1899
+ env.currentProject
1900
+ )
1276
1901
  );
1277
1902
  console.log(jsonFormatter(block));
1278
1903
 
1279
- if (!this.contensis) await this.ConnectContensis();
1280
- if (this.contensis) {
1904
+ const contensis = await this.ConnectContensis();
1905
+ if (contensis) {
1281
1906
  // Push new block version
1282
- const [err, blockVersion] = await this.contensis.blocks.PushBlockVersion(
1907
+ const [err, blockVersion] = await contensis.blocks.PushBlockVersion(
1283
1908
  block
1284
1909
  );
1285
1910
  if (!err) {
@@ -1291,7 +1916,6 @@ class ContensisCli {
1291
1916
  env.currentProject
1292
1917
  )
1293
1918
  );
1294
- console.log(jsonFormatter(blockVersion));
1295
1919
  }
1296
1920
  if (blockVersion) {
1297
1921
  this.HandleFormattingAndOutput(blockVersion, () => {
@@ -1303,6 +1927,113 @@ class ContensisCli {
1303
1927
  throw new Error(
1304
1928
  messages.blocks.failedPush(block.id, currentEnv, env.currentProject)
1305
1929
  );
1930
+ } else {
1931
+ throw new Error(
1932
+ messages.blocks.failedPush(block.id, currentEnv, env.currentProject)
1933
+ );
1934
+ }
1935
+ };
1936
+
1937
+ GetLatestBlockVersion = async (
1938
+ blockId: string,
1939
+ branch = 'default'
1940
+ ): Promise<[AppError | null, string | undefined]> => {
1941
+ const { contensis, log, messages } = this;
1942
+
1943
+ // Look for block versions pushed to "default" branch
1944
+ const [getErr, blockVersions] =
1945
+ (await contensis?.blocks.GetBlockVersions(blockId, branch)) || [];
1946
+
1947
+ if (getErr) {
1948
+ return [getErr, undefined];
1949
+ }
1950
+
1951
+ // Parse versionNo from response
1952
+ let blockVersionNo = 'latest';
1953
+ // The first blockVersion should be the latest one
1954
+ try {
1955
+ blockVersionNo = `${blockVersions?.[0]?.version.versionNo}`;
1956
+
1957
+ if (!Number.isNaN(blockVersionNo) && Number(blockVersionNo) > 0)
1958
+ // Is a valid versionNo
1959
+ return [null, blockVersionNo];
1960
+ else throw new Error(`'${blockVersionNo}' is not a valid version number`);
1961
+ } catch (parseVersionEx: any) {
1962
+ // Catch parsing errors in case of an unexpected response
1963
+ log.info(
1964
+ `Request for blockId: ${blockId}, branch: ${branch}, version: latest`
1965
+ );
1966
+ log.info(
1967
+ `Get block versions response was: ${tryStringify(blockVersions)}`
1968
+ );
1969
+ log.error(messages.blocks.failedParsingVersion());
1970
+ return [parseVersionEx, undefined];
1971
+ }
1972
+ };
1973
+
1974
+ ExecuteBlockAction = async (
1975
+ action: BlockActionType,
1976
+ blockId: string,
1977
+ version = 'latest'
1978
+ ) => {
1979
+ const { currentEnv, env, log, messages } = this;
1980
+ const contensis = await this.ConnectContensis();
1981
+ if (contensis) {
1982
+ let actionOnBlockVersion = version;
1983
+
1984
+ // If action is release and version is latest, find the latest version number
1985
+ if (action === 'release' && version === 'latest') {
1986
+ const [getErr, blockVersion] = await this.GetLatestBlockVersion(
1987
+ blockId
1988
+ );
1989
+
1990
+ if (getErr) {
1991
+ // Log error getting latest block version no
1992
+ // and throw the error message so the process can exit with a failure
1993
+ throw new Error(
1994
+ `${messages.blocks.noList(
1995
+ currentEnv,
1996
+ env.currentProject
1997
+ )} (${getErr})`
1998
+ );
1999
+ } else if (blockVersion) {
2000
+ actionOnBlockVersion = blockVersion;
2001
+ }
2002
+ }
2003
+
2004
+ // Execute block action
2005
+ const [err, blockVersion] = await contensis.blocks.BlockAction(
2006
+ blockId,
2007
+ action,
2008
+ actionOnBlockVersion
2009
+ );
2010
+
2011
+ if (blockVersion) {
2012
+ this.HandleFormattingAndOutput(blockVersion, () => {
2013
+ // print the version detail to console
2014
+ log.success(
2015
+ messages.blocks.actionComplete(
2016
+ action,
2017
+ blockId,
2018
+ currentEnv,
2019
+ env.currentProject
2020
+ )
2021
+ );
2022
+ printBlockVersion(this, blockVersion);
2023
+ });
2024
+ }
2025
+
2026
+ if (err) {
2027
+ log.error(jsonFormatter(err));
2028
+ throw new Error(
2029
+ messages.blocks.actionFailed(
2030
+ action,
2031
+ blockId,
2032
+ currentEnv,
2033
+ env.currentProject
2034
+ )
2035
+ );
2036
+ }
1306
2037
  }
1307
2038
  };
1308
2039
 
@@ -1310,72 +2041,241 @@ class ContensisCli {
1310
2041
  blockId: string,
1311
2042
  branch: string,
1312
2043
  version: string,
1313
- dataCenter: 'hq' | 'manchester' | 'london'
2044
+ dataCenter: 'hq' | 'manchester' | 'london' | undefined,
2045
+ follow = false
1314
2046
  ) => {
1315
- const { currentEnv, log, messages } = this;
1316
- if (!this.contensis) await this.ConnectContensis();
1317
- if (this.contensis) {
2047
+ const { currentEnv, env, log, messages } = this;
2048
+ const contensis = await this.ConnectContensis();
2049
+ if (contensis) {
1318
2050
  // Retrieve block logs
1319
- const [err, blockLogs] = await this.contensis.blocks.GetBlockLogs({
2051
+ log.success(
2052
+ messages.blocks.getLogs(blockId, branch, currentEnv, env.currentProject)
2053
+ );
2054
+
2055
+ const [err, blockLogs] = await contensis.blocks.GetBlockLogs({
1320
2056
  blockId,
1321
2057
  branchId: branch,
1322
2058
  version,
1323
2059
  dataCenter,
1324
2060
  });
1325
2061
 
1326
- if (blockLogs) {
1327
- this.HandleFormattingAndOutput(blockLogs, () => {
1328
- // print the logs to console
1329
- log.success(messages.blocks.list(currentEnv));
2062
+ if (err) {
2063
+ log.error(
2064
+ messages.blocks.failedGetLogs(blockId, currentEnv, env.currentProject)
2065
+ );
2066
+ log.error(jsonFormatter(err));
2067
+ } else if (blockLogs) {
2068
+ const removeTrailingNewline = (logs: string) =>
2069
+ logs.endsWith('\n') ? logs.slice(0, logs.length - 1) : logs;
2070
+ const renderLogs = removeTrailingNewline(blockLogs);
2071
+
2072
+ this.HandleFormattingAndOutput(renderLogs, () => {
2073
+ // print the logs to console
1330
2074
  console.log(
1331
2075
  ` - ${blockId} ${branch} ${
1332
2076
  Number(version) ? `v${version}` : version
1333
- } [${dataCenter}]`
2077
+ } ${dataCenter ? `[${dataCenter}]` : ''}`
1334
2078
  );
1335
2079
  log.line();
1336
- console.log(log.infoText(blockLogs));
1337
- log.line();
2080
+ console.log(log.infoText(renderLogs));
2081
+ });
2082
+
2083
+ // Code for the `--follow` options
2084
+ let following = follow;
2085
+ let alreadyShown = blockLogs;
2086
+ let needsNewLine = false;
2087
+ let counter = 0;
2088
+
2089
+ // remove existing listeners and add them back afterwards
2090
+ const listeners = process.listeners('SIGINT');
2091
+
2092
+ process.removeAllListeners('SIGINT');
2093
+ // add listener to update following to false and break out
2094
+ process.on('SIGINT', () => {
2095
+ Logger.warning(
2096
+ messages.blocks.stopFollow(blockId, currentEnv, env.currentProject)
2097
+ );
2098
+ stopFollowing();
2099
+ });
2100
+
2101
+ let delay = promiseDelay(5 * 1000, null);
2102
+ const stopFollowing = () => {
2103
+ following = false;
2104
+ delay.cancel();
2105
+
2106
+ // Add back the listeners we removed previously
2107
+ process.removeAllListeners('SIGINT');
2108
+ for (const listener of listeners)
2109
+ process.addListener('SIGINT', listener);
2110
+ };
2111
+
2112
+ while (following) {
2113
+ if (counter++ > 300) {
2114
+ Logger.warning(
2115
+ messages.blocks.timeoutFollow(
2116
+ blockId,
2117
+ currentEnv,
2118
+ env.currentProject
2119
+ )
2120
+ );
2121
+ stopFollowing();
2122
+ }
2123
+
2124
+ // wait n. seconds then poll for logs again
2125
+ await delay.wait();
2126
+
2127
+ const [lastErr, lastLogs] = following
2128
+ ? await contensis.blocks.GetBlockLogs({
2129
+ blockId,
2130
+ branchId: branch,
2131
+ version,
2132
+ dataCenter,
2133
+ })
2134
+ : [null, null];
2135
+
2136
+ if (lastLogs) {
2137
+ // Find the difference and output it next
2138
+ const difference = diffLogStrings(lastLogs, alreadyShown);
2139
+ if (difference) {
2140
+ if (needsNewLine) {
2141
+ console.log('');
2142
+ }
2143
+ // Take the trailing newline off of the logged output to
2144
+ // avoid blank lines inbetween logs fetched sequentially
2145
+ const render = removeTrailingNewline(difference);
2146
+ console.log(log.infoText(render));
2147
+
2148
+ // Add what we've just rendered to already shown "cache"
2149
+ alreadyShown += `${render}\n`;
2150
+ needsNewLine = false;
2151
+ } else {
2152
+ // If no difference output a dot
2153
+ process.stdout.write('.');
2154
+ needsNewLine = true;
2155
+ }
2156
+ } else if (lastErr) {
2157
+ // If error output an x
2158
+ process.stdout.write('x');
2159
+ needsNewLine = true;
2160
+ }
2161
+ }
2162
+ }
2163
+ }
2164
+ };
2165
+
2166
+ PrintProxies = async (proxyId?: string) => {
2167
+ const { currentEnv, env, log, messages } = this;
2168
+ const contensis = await this.ConnectContensis();
2169
+ if (contensis) {
2170
+ // Retrieve proxies list for env
2171
+ const [err, proxies] = await contensis.proxies.GetProxies(proxyId);
2172
+
2173
+ if (Array.isArray(proxies)) {
2174
+ this.HandleFormattingAndOutput(proxies, () => {
2175
+ // print the proxies to console
2176
+ log.success(messages.proxies.list(currentEnv, env.currentProject));
2177
+ for (const { id, name, description, endpoints, version } of proxies) {
2178
+ console.log(
2179
+ ` - ${name} [${
2180
+ version.versionNo
2181
+ }] ${id} ${log.infoText`${description}`}`
2182
+ );
2183
+ for (const [language, endpoint] of Object.entries(endpoints))
2184
+ console.log(
2185
+ ` - ${log.infoText`language: ${language}
2186
+ server: ${endpoint.server}
2187
+ headers.host: ${endpoint.headers.host}
2188
+ ssl: ${endpoint.ssl}`}`
2189
+ );
2190
+ }
1338
2191
  });
1339
2192
  }
1340
2193
 
1341
2194
  if (err) {
1342
- log.error(messages.blocks.noList(currentEnv));
2195
+ log.error(messages.proxies.noList(currentEnv, env.currentProject));
1343
2196
  log.error(jsonFormatter(err));
1344
2197
  }
1345
2198
  }
1346
2199
  };
1347
2200
 
2201
+ PrintRenderers = async (rendererId?: string) => {
2202
+ const { currentEnv, env, log, messages } = this;
2203
+ const contensis = await this.ConnectContensis();
2204
+ if (contensis) {
2205
+ // Retrieve renderers list for env
2206
+ const [err, renderers] = await contensis.renderers.GetRenderers(
2207
+ rendererId
2208
+ );
2209
+
2210
+ if (Array.isArray(renderers)) {
2211
+ this.HandleFormattingAndOutput(renderers, () => {
2212
+ // print the renderers to console
2213
+ log.success(messages.renderers.list(currentEnv, env.currentProject));
2214
+ for (const {
2215
+ id,
2216
+ description,
2217
+ assignedContentTypes,
2218
+ rules,
2219
+ version,
2220
+ } of renderers) {
2221
+ console.log(
2222
+ ` - ${id} [${version.versionNo}] ${log.infoText`${description}`}`
2223
+ );
2224
+ if (assignedContentTypes?.length)
2225
+ console.log(
2226
+ log.infoText` assignedContentTypes: ${assignedContentTypes.join(
2227
+ ', '
2228
+ )}`
2229
+ );
2230
+ for (const rule of rules)
2231
+ if (rule.return)
2232
+ console.log(
2233
+ log.infoText` ${
2234
+ rule.return.endpointId ? 'endpointId' : 'blockId'
2235
+ }: ${rule.return.endpointId || rule.return.blockId}`
2236
+ );
2237
+ }
2238
+ });
2239
+ }
2240
+
2241
+ if (err) {
2242
+ log.error(messages.renderers.noList(currentEnv, env.currentProject));
2243
+ log.error(jsonFormatter(err));
2244
+ }
2245
+ }
2246
+ };
1348
2247
  HandleFormattingAndOutput = <T>(obj: T, logFn: (obj: T) => void) => {
1349
2248
  const { format, log, messages, output } = this;
2249
+ if (!format) {
2250
+ // print the object to console
2251
+ logFn(obj);
2252
+ } else if (format === 'csv') {
2253
+ log.raw('');
2254
+ log.raw(log.infoText(csvFormatter(obj)));
2255
+ } else if (format === 'xml') {
2256
+ log.raw('');
2257
+ log.raw(log.infoText(xmlFormatter(obj)));
2258
+ } else if (format === 'json') {
2259
+ log.raw('');
2260
+ log.raw(log.infoText(jsonFormatter(obj)));
2261
+ }
2262
+ log.raw('');
2263
+
1350
2264
  if (output) {
1351
2265
  let writeString = '';
2266
+ const isText = !tryParse(obj) && typeof obj === 'string';
1352
2267
  if (format === 'csv') {
1353
2268
  writeString = csvFormatter(obj as any);
1354
2269
  } else if (format === 'xml') {
1355
2270
  writeString = xmlFormatter(obj as any);
1356
- } else writeString = jsonFormatter(obj);
2271
+ } else writeString = isText ? (obj as string) : jsonFormatter(obj);
1357
2272
  // write output to file
1358
2273
  if (writeString) {
1359
2274
  fs.writeFileSync(output, writeString);
1360
- log.success(messages.app.fileOutput(format, output));
2275
+ log.success(messages.app.fileOutput(isText ? 'text' : format, output));
1361
2276
  } else {
1362
2277
  log.info(messages.app.noFileOutput());
1363
2278
  }
1364
- } else {
1365
- if (!format) {
1366
- // print the object to console
1367
- logFn(obj);
1368
- } else if (format === 'csv') {
1369
- log.raw('');
1370
- log.raw(log.infoText(csvFormatter(obj)));
1371
- } else if (format === 'xml') {
1372
- log.raw('');
1373
- log.raw(log.infoText(xmlFormatter(obj)));
1374
- } else if (format === 'json') {
1375
- log.raw('');
1376
- log.raw(log.infoText(jsonFormatter(obj)));
1377
- }
1378
- log.raw('');
1379
2279
  }
1380
2280
  };
1381
2281
  }