contensis-cli 1.0.0-beta.83 → 1.0.0-beta.85

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.
@@ -4,7 +4,9 @@ import {
4
4
  MigrateModelsResult,
5
5
  MigrateStatus,
6
6
  } from 'migratortron';
7
+ import { GitHelper } from '~/util/git';
7
8
  import { Logger } from '~/util/logger';
9
+ import { winSlash } from '~/util/os';
8
10
 
9
11
  export const LogMessages = {
10
12
  app: {
@@ -411,4 +413,87 @@ export const LogMessages = {
411
413
  id
412
414
  )}`,
413
415
  },
416
+ devinit: {
417
+ intro: () => `Contensis developer environment initialisation`,
418
+ //`This will initialise your local working directory to develop with the current connected Contensis project`,
419
+ projectDetails: (
420
+ name: string,
421
+ env: string,
422
+ projectId: string,
423
+ git: GitHelper
424
+ ) =>
425
+ `Project: ${Logger.highlightText(name)} set arg --name to override
426
+ - Home: ${Logger.standardText(process.cwd())}
427
+ - Repository: ${git.home}
428
+
429
+ Connect to Contensis instance: ${Logger.standardText(env)}
430
+ - Project id: ${Logger.standardText(projectId)}`,
431
+ developmentKey: (name: string, existing: boolean) =>
432
+ ` - ${
433
+ !existing ? 'Create development API key' : 'Development API key found'
434
+ }: ${Logger[!existing ? 'highlightText' : 'standardText'](name)}`,
435
+ deploymentKey: (name: string, existing: boolean) =>
436
+ ` - ${
437
+ !existing ? 'Create deployment API key' : 'Deployment API key found'
438
+ }: ${Logger[!existing ? 'highlightText' : 'standardText'](name)}`,
439
+ ciIntro: (git: GitHelper) =>
440
+ `We will create API keys with permissions to use this project with Contensis, and add a job to your CI that will deploy a container build. We will ask you to add secrets/variables to your git repository to give your workflow permission to push a Block to Contensis.
441
+
442
+ You could visit ${git.secretsUri} to check that you can see repository settings`,
443
+ ciDetails: (filename: string) =>
444
+ `Add push-block job to CI file: ${Logger.highlightText(filename)}\n`,
445
+ confirm: () =>
446
+ `Confirm these details are correct so we can make changes to your project`,
447
+ accessTokenPrompt: () =>
448
+ `Please supply the access token for the Delivery API (optional)`,
449
+ createDevKey: (keyName: string, existing: boolean) =>
450
+ `${
451
+ !existing ? 'Created' : 'Checked permissions for'
452
+ } development API key ${Logger.standardText(keyName)}`,
453
+ createDeployKey: (keyName: string, existing: boolean) =>
454
+ `${
455
+ !existing ? 'Created' : 'Checked permissions for'
456
+ } deployment API key ${Logger.standardText(keyName)}`,
457
+ createKeyFail: (keyName: string, existing: boolean) =>
458
+ `Failed to ${
459
+ !existing ? 'create' : 'update'
460
+ } API key ${Logger.highlightText(keyName)}`,
461
+ writeEnvFile: () => `Written .env file to project home directory`,
462
+ useEnvFileTip: () =>
463
+ `You should alter existing project code that connects a Contensis client to use the variables from this file`,
464
+ writeCiFile: (ciFilePath: string) =>
465
+ `Updated CI file ${Logger.standardText(winSlash(ciFilePath))}`,
466
+ ciBlockTip: (blockId: string, env: string, projectId: string) =>
467
+ `A job is included to deploy your built container image to ${Logger.standardText(
468
+ projectId
469
+ )} at ${Logger.standardText(env)} in a block called ${Logger.standardText(
470
+ blockId
471
+ )}`,
472
+ addGitSecretsIntro: () =>
473
+ `We have ceated an API key that allows you to deploy your app image to a Contensis Block but we need you to add these details to your GitLab repository.`,
474
+ addGitSecretsHelp: (git: GitHelper, id: string, secret: string) =>
475
+ `Add secrets or variables in your repository's settings page\n\nGo to ${Logger.highlightText(
476
+ git.secretsUri
477
+ )}\n\n${
478
+ git.type === 'github'
479
+ ? `Add a "New repository secret"`
480
+ : `Expand "Variables" and hit "Add variable"`
481
+ }\n\n ${
482
+ git.type === 'github' ? `Secret name:` : `Key:`
483
+ } ${Logger.highlightText(`CONTENSIS_CLIENT_ID`)}\n ${
484
+ git.type === 'github' ? `Secret:` : `Value:`
485
+ } ${Logger.highlightText(
486
+ id
487
+ )}\n\n ${`Add one more secret/variable to the repository`}\n\n ${
488
+ git.type === 'github' ? `Secret name:` : `Key:`
489
+ } ${Logger.highlightText(`CONTENSIS_SHARED_SECRET`)}\n ${
490
+ git.type === 'github' ? `Secret:` : `Value:`
491
+ } ${Logger.highlightText(secret)}`,
492
+ success: () => `Contensis developer environment initialisation complete`,
493
+ partialSuccess: () =>
494
+ `Contensis developer environment initialisation completed with errors`,
495
+ failed: () => `Contensis developer environment initialisation failed`,
496
+ startProjectTip: () =>
497
+ `Start up your project in the normal way for development`,
498
+ },
414
499
  };
@@ -0,0 +1,44 @@
1
+ import ContensisCli from '~/services/ContensisCliService';
2
+
3
+ interface ISiteConfigYaml {
4
+ alias: string;
5
+ projectId: string;
6
+ accessToken: string;
7
+ clientId: string;
8
+ sharedSecret: string;
9
+ blocks: {
10
+ id: string;
11
+ baseUri: string;
12
+ }[];
13
+ }
14
+
15
+ export const mapSiteConfigYaml = async (cli: ContensisCli) => {
16
+ const credentials = await cli.GetCredentials(cli.env.lastUserId);
17
+
18
+ const blocks = [];
19
+ const blocksRaw = await cli.PrintBlocks();
20
+
21
+ for (const block of blocksRaw || []) {
22
+ const versions = await cli.PrintBlockVersions(
23
+ block.id,
24
+ 'default',
25
+ 'latest'
26
+ );
27
+ if (versions?.[0]) {
28
+ blocks.push({
29
+ id: versions[0].id,
30
+ baseUri: versions[0].previewUrl,
31
+ });
32
+ }
33
+ }
34
+
35
+ const siteConfig: ISiteConfigYaml = {
36
+ alias: cli.currentEnv,
37
+ projectId: cli.currentProject,
38
+ accessToken: '',
39
+ clientId: credentials?.current?.account || '',
40
+ sharedSecret: credentials?.current?.password || '',
41
+ blocks,
42
+ };
43
+ return siteConfig;
44
+ };
@@ -1 +1,2 @@
1
+ declare module 'giturl';
1
2
  declare module 'inquirer-command-prompt';
@@ -25,14 +25,17 @@ export const readFile = (filePath: string) => {
25
25
  }
26
26
  };
27
27
 
28
- export const readFiles = (directory: string) => {
28
+ export const readFiles = (directory: string, createDirectory = true) => {
29
29
  const directoryPath = localPath(directory);
30
30
  if (fs.existsSync(directoryPath)) {
31
31
  const files = fs.readdirSync(directoryPath);
32
32
  return files;
33
- } else {
33
+ } else if (createDirectory) {
34
34
  fs.mkdirSync(directoryPath, { recursive: true });
35
35
  return [];
36
+ } else {
37
+ throw new Error(`ENOENT: Directory does not exist ${directoryPath}`);
38
+ // return undefined;
36
39
  }
37
40
  };
38
41
 
@@ -1,4 +1,3 @@
1
- import { execFile, spawn } from 'child_process';
2
1
  import fs from 'fs';
3
2
  import path from 'path';
4
3
  import fetch from 'node-fetch';
@@ -24,7 +23,7 @@ import ContensisAuthService from './ContensisAuthService';
24
23
 
25
24
  import { LogMessages } from '~/localisation/en-GB';
26
25
 
27
- import { appRootDir, readJsonFile } from '~/providers/file-provider';
26
+ import { readJsonFile } from '~/providers/file-provider';
28
27
  import SessionCacheProvider from '../providers/SessionCacheProvider';
29
28
  import CredentialProvider from '~/providers/CredentialProvider';
30
29
 
@@ -49,6 +48,7 @@ import { diffLogStrings } from '~/util/diff';
49
48
  import { logError, Logger } from '~/util/logger';
50
49
  import { promiseDelay } from '~/util/timers';
51
50
 
51
+
52
52
  type OutputFormat = 'json' | 'csv' | 'xml';
53
53
 
54
54
  type OutputOptions = {
@@ -73,6 +73,17 @@ interface IImportOptions {
73
73
  sourceProjectId?: string;
74
74
  }
75
75
 
76
+ export type OutputOptionsConstructorArg = OutputOptions &
77
+ IConnectOptions &
78
+ IImportOptions;
79
+
80
+ export interface ContensisCliConstructor {
81
+ new (
82
+ args: string[],
83
+ outputOpts?: OutputOptionsConstructorArg,
84
+ contensisOpts?: Partial<MigrateRequest>
85
+ ): ContensisCli;
86
+ }
76
87
  let insecurePasswordWarningShown = false;
77
88
 
78
89
  class ContensisCli {
@@ -143,7 +154,12 @@ class ContensisCli {
143
154
 
144
155
  constructor(
145
156
  args: string[],
146
- outputOpts?: OutputOptions & IConnectOptions & IImportOptions,
157
+ outputOpts?: OutputOptionsConstructorArg,
158
+ contensisOpts?: Partial<MigrateRequest>
159
+ );
160
+ constructor(
161
+ args: string[],
162
+ outputOpts?: OutputOptionsConstructorArg,
147
163
  contensisOpts: Partial<MigrateRequest> = {}
148
164
  ) {
149
165
  // console.log('args: ', JSON.stringify(args, null, 2));
@@ -204,80 +220,6 @@ class ContensisCli {
204
220
  }
205
221
  }
206
222
 
207
- ExecRequestHandler = async (blockIds: string[], overrideArgs?: string[]) => {
208
- const { log } = this;
209
- // const getPrefixOld = log.getPrefix;
210
- const exeHome = path.join(appRootDir, 'reqhan');
211
- const exe = 'Zengenti.Contensis.RequestHandler.LocalDevelopment';
212
- const exePath = path.join(exeHome, exe);
213
- const siteConfigPath = path.join(appRootDir, 'site_config.yaml');
214
-
215
- const args = overrideArgs
216
- ? typeof overrideArgs?.[0] === 'string' &&
217
- overrideArgs[0].includes(' ', 2)
218
- ? overrideArgs[0].split(' ')
219
- : overrideArgs
220
- : []; // args could be [ '-c .\\site_config.yaml' ] or [ '-c', '.\\site_config.yaml' ]
221
-
222
- // Add required args
223
- if (!args.find(a => a === '-c')) args.push('-c', siteConfigPath);
224
-
225
- // const child = execFile(exePath, args);
226
-
227
- const child = spawn(exePath, args, { stdio: 'inherit' });
228
-
229
- log.raw('');
230
- log.info(`Launching request handler...`);
231
- if (overrideArgs?.length)
232
- this.log.warning(
233
- `Spawning process: ${JSON.stringify(child.spawnargs, null, 2)}`
234
- );
235
-
236
- let isRunning = false;
237
-
238
- // Log child output through event listeners
239
- child?.stdout?.on('data', data => {
240
- isRunning = true;
241
- log.raw(data);
242
- });
243
-
244
- child?.stderr?.on('data', data => {
245
- log.error(data);
246
- });
247
-
248
- child.on('spawn', () => {
249
- isRunning = true;
250
- log.help(
251
- `You may see a firewall popup requesting network access, it is safe to approve`
252
- );
253
- // log.getPrefix = () => Logger.infoText(`[rqh]`);
254
- });
255
-
256
- child.on('exit', code => {
257
- isRunning = false;
258
-
259
- log[code === 0 ? 'success' : 'warning'](
260
- `Request handler exited with code ${code}\n`
261
- );
262
- });
263
-
264
- child.on('error', error => {
265
- isRunning = false;
266
- log.error(`Could not launch request handler due to error \n${error}`);
267
- });
268
-
269
- await new Promise(resolve => setTimeout(resolve, 2000));
270
-
271
- // keep the method running until we can return
272
- while (true === true) {
273
- if (!isRunning) {
274
- // log.getPrefix = getPrefixOld; // restore logger state
275
- return;
276
- }
277
- await new Promise(resolve => setTimeout(resolve, 1000));
278
- }
279
- };
280
-
281
223
  PrintEnvironments = () => {
282
224
  const { log, messages } = this;
283
225
  const { currentEnvironment, environments = {} } = this.cache;
@@ -1913,6 +1855,8 @@ class ContensisCli {
1913
1855
  );
1914
1856
  }
1915
1857
  });
1858
+
1859
+ return blocks;
1916
1860
  }
1917
1861
 
1918
1862
  if (err) {
@@ -1957,6 +1901,8 @@ class ContensisCli {
1957
1901
  : undefined
1958
1902
  );
1959
1903
  });
1904
+
1905
+ return blocks;
1960
1906
  }
1961
1907
 
1962
1908
  if (err) {
@@ -2361,7 +2307,7 @@ class ContensisCli {
2361
2307
 
2362
2308
  export const cliCommand = (
2363
2309
  commandArgs: string[],
2364
- outputOpts: OutputOptions & IConnectOptions = {},
2310
+ outputOpts: OutputOptionsConstructorArg,
2365
2311
  contensisOpts: Partial<MigrateRequest> = {}
2366
2312
  ) => {
2367
2313
  return new ContensisCli(['', '', ...commandArgs], outputOpts, contensisOpts);
@@ -0,0 +1,243 @@
1
+ import { execFile, spawn } from 'child_process';
2
+ import inquirer from 'inquirer';
3
+ import path from 'path';
4
+
5
+ import { MigrateRequest } from 'migratortron';
6
+ import { stringify } from 'yaml';
7
+
8
+ import ContensisCli, {
9
+ OutputOptionsConstructorArg,
10
+ } from './ContensisCliService';
11
+ import { mapSiteConfigYaml } from '~/mappers/ContensisCliService-to-RequestHanderSiteConfigYaml';
12
+ import { appRootDir, writeFile } from '~/providers/file-provider';
13
+ import { jsonFormatter } from '~/util/json.formatter';
14
+ import { GitHelper } from '~/util/git';
15
+
16
+ class ContensisDev extends ContensisCli {
17
+ constructor(
18
+ args: string[],
19
+ outputOpts?: OutputOptionsConstructorArg,
20
+ contensisOpts: Partial<MigrateRequest> = {}
21
+ ) {
22
+ super(args, outputOpts, contensisOpts);
23
+ }
24
+
25
+ DevelopmentInit = async (projectHome: string, opts: any) => {
26
+ const { currentEnv, currentProject, log, messages } = this;
27
+ const contensis = await this.ConnectContensis();
28
+
29
+ if (contensis) {
30
+ // Retrieve keys list for env
31
+ const [keysErr, apiKeys] = await contensis.apiKeys.GetKeys();
32
+ if (keysErr) {
33
+ log.error(messages.keys.noList(currentEnv));
34
+ log.error(jsonFormatter(keysErr));
35
+ return;
36
+ }
37
+ const apiKeyExists = (findKey: string) =>
38
+ apiKeys?.find(
39
+ k => k.name.trim().toLowerCase() === findKey?.trim().toLowerCase()
40
+ );
41
+
42
+ // Retrieve git info
43
+ const git = new GitHelper(projectHome);
44
+
45
+ // Retrieve ci workflow info
46
+ const workflowFiles = git.workflows;
47
+
48
+ // Set variables for logging etc.
49
+ let ciFileName = git.ciFileName;
50
+
51
+ const devKey = `${git.name} development`;
52
+ const deployKey = `${git.name} deployment`;
53
+ const blockId = git.name;
54
+
55
+ // Start render console output
56
+ log.raw('');
57
+ log.success(messages.devinit.intro());
58
+ log.raw('');
59
+ log.raw(
60
+ log.infoText(
61
+ messages.devinit.projectDetails(
62
+ git.name,
63
+ currentEnv,
64
+ currentProject,
65
+ git
66
+ )
67
+ )
68
+ );
69
+ log.raw(
70
+ log.infoText(
71
+ messages.devinit.developmentKey(devKey, !!apiKeyExists(devKey))
72
+ )
73
+ );
74
+ log.raw(
75
+ log.infoText(
76
+ messages.devinit.deploymentKey(deployKey, !!apiKeyExists(deployKey))
77
+ )
78
+ );
79
+ log.raw('');
80
+
81
+ if (Array.isArray(workflowFiles) && workflowFiles.length > 1) {
82
+ // Choose GitHub workflow file (if multiple)
83
+ ({ ciFileName } = await inquirer.prompt([
84
+ {
85
+ type: 'list',
86
+ message: `Multiple GitHub workflow files found\n${log.infoText(
87
+ `Tell us which GitHub workflow builds the container image after each push:`
88
+ )}`,
89
+ name: 'ciFileName',
90
+ choices: workflowFiles,
91
+ default: workflowFiles.find(f => f.includes('docker')),
92
+ },
93
+ ]));
94
+ log.raw('');
95
+ }
96
+
97
+ log.raw(log.infoText(messages.devinit.ciDetails(ciFileName)));
98
+ log.help(messages.devinit.ciIntro(git));
99
+
100
+ // Confirm prompt
101
+ const { confirm } = await inquirer.prompt([
102
+ {
103
+ type: 'confirm',
104
+ message: messages.devinit.confirm(),
105
+ name: 'confirm',
106
+ default: false,
107
+ },
108
+ ]);
109
+ log.raw('');
110
+ if (!confirm) return;
111
+
112
+ // Access token prompt
113
+ const { accessToken } = await inquirer.prompt([
114
+ {
115
+ type: 'input',
116
+ message: messages.devinit.accessTokenPrompt(),
117
+ name: 'accessToken',
118
+ },
119
+ ]);
120
+ log.raw('');
121
+
122
+ // Magic happens...
123
+
124
+ // Arrange API keys for development and deployment
125
+ log.success(messages.devinit.createDevKey(devKey, false));
126
+ log.success(messages.devinit.createDeployKey(deployKey, true));
127
+
128
+ // Update or create a file called .env in project home
129
+ log.success(messages.devinit.writeEnvFile());
130
+ // log.help(messages.devinit.useEnvFileTip());
131
+
132
+ // Update CI file -- different for GH/GL -- create a sample one with build?
133
+ log.success(messages.devinit.writeCiFile(`./${ciFileName}`));
134
+ log.info(
135
+ messages.devinit.ciBlockTip(blockId, currentEnv, currentProject)
136
+ );
137
+
138
+ // Echo Deployment API key to console, ask user to add secrets to repo
139
+ log.warning(messages.devinit.addGitSecretsIntro());
140
+ log.help(
141
+ messages.devinit.addGitSecretsHelp(git, '123-456', '789-012-345')
142
+ );
143
+
144
+ log.success(messages.devinit.success());
145
+ log.help(messages.devinit.startProjectTip());
146
+ }
147
+ };
148
+
149
+ ExecRequestHandler = async (blockIds: string[], overrideArgs?: string[]) => {
150
+ // if no request handler exe
151
+ // download it.
152
+
153
+ // if update arg, redownload it
154
+
155
+ const { log } = this;
156
+ // const getPrefixOld = log.getPrefix;
157
+ const exeHome = path.join(appRootDir, 'reqhan');
158
+ const exe = 'Zengenti.Contensis.RequestHandler.LocalDevelopment';
159
+ const exePath = path.join(exeHome, exe);
160
+ const siteConfigPath = path.join(appRootDir, 'site_config.yaml');
161
+
162
+ const siteConfig = await mapSiteConfigYaml(this);
163
+ writeFile('site_config.yaml', stringify(siteConfig));
164
+
165
+ const args = overrideArgs
166
+ ? typeof overrideArgs?.[0] === 'string' &&
167
+ overrideArgs[0].includes(' ', 2)
168
+ ? overrideArgs[0].split(' ')
169
+ : overrideArgs
170
+ : []; // args could be [ '-c .\\site_config.yaml' ] or [ '-c', '.\\site_config.yaml' ]
171
+
172
+ // Add required args
173
+ if (!args.find(a => a === '-c')) args.push('-c', siteConfigPath);
174
+
175
+ // const child = execFile(exePath, args);
176
+
177
+ const child = spawn(exePath, args, { stdio: 'inherit' });
178
+
179
+ // log.raw('');
180
+ log.info(`Launching request handler...`);
181
+ if (overrideArgs?.length)
182
+ this.log.warning(
183
+ `Spawning process with supplied args: ${JSON.stringify(
184
+ child.spawnargs,
185
+ null,
186
+ 2
187
+ )}`
188
+ );
189
+
190
+ let isRunning = false;
191
+
192
+ // Log child output through event listeners
193
+ child?.stdout?.on('data', data => {
194
+ isRunning = true;
195
+ log.raw(data);
196
+ });
197
+
198
+ child?.stderr?.on('data', data => {
199
+ log.error(data);
200
+ });
201
+
202
+ child.on('spawn', () => {
203
+ isRunning = true;
204
+ log.help(
205
+ `You may see a firewall popup requesting network access, it is safe to approve`
206
+ );
207
+ // log.getPrefix = () => Logger.infoText(`[rqh]`);
208
+ });
209
+
210
+ child.on('exit', code => {
211
+ isRunning = false;
212
+
213
+ log[code === 0 ? 'success' : 'warning'](
214
+ `Request handler exited with code ${code}\n`
215
+ );
216
+ });
217
+
218
+ child.on('error', error => {
219
+ isRunning = false;
220
+ log.error(`Could not launch request handler due to error \n${error}`);
221
+ });
222
+
223
+ await new Promise(resolve => setTimeout(resolve, 2000));
224
+
225
+ // keep the method running until we can return
226
+ while (true === true) {
227
+ if (!isRunning) {
228
+ // log.getPrefix = getPrefixOld; // restore logger state
229
+ return;
230
+ }
231
+ await new Promise(resolve => setTimeout(resolve, 1000));
232
+ }
233
+ };
234
+ }
235
+ export const devCommand = (
236
+ commandArgs: string[],
237
+ outputOpts: OutputOptionsConstructorArg,
238
+ contensisOpts: Partial<MigrateRequest> = {}
239
+ ) => {
240
+ return new ContensisDev(['', '', ...commandArgs], outputOpts, contensisOpts);
241
+ };
242
+
243
+ export default ContensisDev;
package/src/shell.ts CHANGED
@@ -259,16 +259,7 @@ export const shell = () => {
259
259
  // after successful connect / login / set project
260
260
  if (typeof process.argv?.[2] !== 'undefined')
261
261
  return {
262
- quit(error?: Error) {
263
- process.removeAllListeners('exit');
264
-
265
- if (error) {
266
- Logger.error(error.message);
267
- process.exit(1);
268
- } else {
269
- process.exit(0);
270
- }
271
- },
262
+ quit: ContensisCli.quit,
272
263
  restart() {},
273
264
  } as any;
274
265
  if (!globalShell) globalShell = new ContensisShell();
@@ -282,9 +273,9 @@ process.on('uncaughtException', function (err) {
282
273
 
283
274
  process.on('SIGINT', () => {
284
275
  Logger.warning('received SIGINT');
276
+ shell().quit();
285
277
  // setTimeout(() => {
286
- // shell().quit();
287
- // }, 1000);
278
+ // }, 2000);
288
279
  });
289
280
 
290
281
  process.on('SIGTERM', () => {