@kumologica/sdk 3.0.0-alpha4

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 (76) hide show
  1. package/README.md +52 -0
  2. package/bin/kl.js +2 -0
  3. package/cli/KumologicaError.js +17 -0
  4. package/cli/cli.js +7 -0
  5. package/cli/commands/build-commands/aws.js +49 -0
  6. package/cli/commands/build-commands/azure.js +43 -0
  7. package/cli/commands/build-commands/kumohub.js +49 -0
  8. package/cli/commands/build.js +6 -0
  9. package/cli/commands/create-commands/create-project-iteratively.js +49 -0
  10. package/cli/commands/create-commands/index.js +5 -0
  11. package/cli/commands/create.js +66 -0
  12. package/cli/commands/deploy-commands/kumohub.js +114 -0
  13. package/cli/commands/deploy.js +6 -0
  14. package/cli/commands/doc-commands/html.js +60 -0
  15. package/cli/commands/doc.js +6 -0
  16. package/cli/commands/export-commands/cloudformation.js +371 -0
  17. package/cli/commands/export-commands/serverless.js +164 -0
  18. package/cli/commands/export-commands/terraform-commands/aws.js +193 -0
  19. package/cli/commands/export-commands/terraform-commands/azure.js +148 -0
  20. package/cli/commands/export-commands/terraform.js +6 -0
  21. package/cli/commands/export-commands/utils/validator.js +195 -0
  22. package/cli/commands/export.js +6 -0
  23. package/cli/commands/list-templates.js +24 -0
  24. package/cli/commands/open.js +53 -0
  25. package/cli/commands/start.js +165 -0
  26. package/cli/commands/test/TestSuiteRunner.js +76 -0
  27. package/cli/commands/test.js +123 -0
  28. package/cli/utils/download-template-from-repo.js +346 -0
  29. package/cli/utils/download-test.js +12 -0
  30. package/cli/utils/download.js +119 -0
  31. package/cli/utils/fs/copy-dir-contents-sync.js +15 -0
  32. package/cli/utils/fs/create-zip-file.js +39 -0
  33. package/cli/utils/fs/dir-exists-sync.js +14 -0
  34. package/cli/utils/fs/dir-exists.js +17 -0
  35. package/cli/utils/fs/file-exists-sync.js +14 -0
  36. package/cli/utils/fs/file-exists.js +12 -0
  37. package/cli/utils/fs/get-tmp-dir-path.js +22 -0
  38. package/cli/utils/fs/parse.js +40 -0
  39. package/cli/utils/fs/read-file-sync.js +11 -0
  40. package/cli/utils/fs/read-file.js +10 -0
  41. package/cli/utils/fs/safe-move-file.js +58 -0
  42. package/cli/utils/fs/walk-dir-sync.js +34 -0
  43. package/cli/utils/fs/write-file-sync.js +31 -0
  44. package/cli/utils/fs/write-file.js +32 -0
  45. package/cli/utils/logger.js +26 -0
  46. package/cli/utils/rename-service.js +49 -0
  47. package/package.json +72 -0
  48. package/src/api/core/comms.js +141 -0
  49. package/src/api/core/context.js +296 -0
  50. package/src/api/core/flows.js +286 -0
  51. package/src/api/core/index.js +29 -0
  52. package/src/api/core/library.js +106 -0
  53. package/src/api/core/nodes.js +476 -0
  54. package/src/api/core/projects.js +426 -0
  55. package/src/api/core/rest/context.js +42 -0
  56. package/src/api/core/rest/flow.js +53 -0
  57. package/src/api/core/rest/flows.js +53 -0
  58. package/src/api/core/rest/index.js +171 -0
  59. package/src/api/core/rest/nodes.js +164 -0
  60. package/src/api/core/rest/util.js +53 -0
  61. package/src/api/core/settings.js +287 -0
  62. package/src/api/tools/base/DesignerTool.js +108 -0
  63. package/src/api/tools/core/flow.js +58 -0
  64. package/src/api/tools/core/index.js +18 -0
  65. package/src/api/tools/core/node.js +77 -0
  66. package/src/api/tools/debugger/index.js +193 -0
  67. package/src/api/tools/filemanager/index.js +127 -0
  68. package/src/api/tools/git/index.js +103 -0
  69. package/src/api/tools/index.js +13 -0
  70. package/src/api/tools/test/index.js +56 -0
  71. package/src/api/tools/test/lib/TestCaseRunner.js +105 -0
  72. package/src/api/tools/test/lib/fixtures/example3-flow.json +148 -0
  73. package/src/api/tools/test/lib/fixtures/package.json +6 -0
  74. package/src/api/tools/test/lib/fixtures/s3-event.js +43 -0
  75. package/src/api/tools/test/lib/reporters/index.js +120 -0
  76. package/src/server/DesignerServer.js +141 -0
@@ -0,0 +1,165 @@
1
+ /**
2
+ * This command should be used to start the kumologica runtime in local mode. And it will
3
+ * be assisting the UI on all aspects of the project development.
4
+ *
5
+ * Example:
6
+ * kl start ./myproject
7
+ * kl start ./myproject/flow.json
8
+ * kl start (this will look for a flow.json in the current directory)
9
+ *
10
+ * If not flow is found, the process will be exited (with code 1) for the time being.
11
+ * Ideally, we want to start the server and serve the Runtime API to allow the user to
12
+ * open/create new projects fromt he UI.
13
+ *
14
+ * The Runtime API can be found in: packages/runtime/src/runtime/lib/api/rest/index.js
15
+ * The actual runtime is in: packages/runtime/src/runtime/lib/index.js
16
+ *
17
+ * Naming convention used during the code:
18
+ * - AdminApp (express app) is the server that serves the Runtime API
19
+ * - NodeApp (express app) is the server that runs the flow
20
+ *
21
+ * WARNING:
22
+ * Current implementation seems to be mounting the runtime api in the NodeApp (see runtime/lib/index.js:534)
23
+ * Ideally we wouldl like to separate the two apps and have the AdminApp running on a different port.
24
+ *
25
+ */
26
+
27
+ const path = require('path');
28
+ const fs = require('fs');
29
+ const { codegen } = require('@kumologica/builder');
30
+ const { DesignerServer } = require('../../src/server/DesignerServer');
31
+ const { logError, logNotice, logInfo, logFatal } = require('../utils/logger');
32
+ // const opn = require('better-opn');
33
+
34
+ exports.command = 'start [project_directory]';
35
+ exports.desc = `Run kumologica runtime in local mode`;
36
+
37
+ exports.builder = (yargs) => {
38
+ yargs.positional(`project_directory`, {
39
+ type: 'string',
40
+ describe:
41
+ 'Path to a valid kumologica project directory or flow file. (Optional)',
42
+ });
43
+ yargs.option(`loglevel`, {
44
+ describe: 'Logging level: [error, warn, info, debug, trace]',
45
+ type: 'string',
46
+ nargs: 1,
47
+ });
48
+ yargs.option(`port`, {
49
+ describe: 'Specifies the listening port utilized by the application',
50
+ type: 'number',
51
+ nargs: 1,
52
+ });
53
+ yargs.option(`adminport`, {
54
+ describe: 'Specifies the listening port utilized by the admin API',
55
+ type: 'number',
56
+ nargs: 1,
57
+ });
58
+ yargs.option(`secured`, {
59
+ describe: 'Enable security control to the admin API',
60
+ type: 'boolean',
61
+ nargs: 1,
62
+ });
63
+ yargs.option(`noadmin`, {
64
+ describe: 'Start the runtime with admin API disabled',
65
+ type: 'boolean',
66
+ nargs: 0,
67
+ });
68
+ };
69
+
70
+ exports.desc = 'Starting Kumologica Runtime for Development';
71
+
72
+ exports.handler = ({
73
+ project_directory,
74
+ loglevel,
75
+ port,
76
+ adminport,
77
+ secured,
78
+ noadmin,
79
+ env
80
+ }) => {
81
+ logNotice(`Launching Kumologica Runtime...`);
82
+ let absProjectDirectory;
83
+
84
+ try {
85
+ absProjectDirectory = fs.realpathSync(project_directory);
86
+ } catch (err) {
87
+ logFatal(`Project directory not found: ${project_directory}`);
88
+ }
89
+
90
+ try {
91
+ // Resolve the path to the flow path
92
+ const [projectFlowDirname, projectFlowFullPath] = resolveProjectFlowPath(absProjectDirectory);
93
+ logInfo(`> Flow file found: ${projectFlowFullPath}`);
94
+
95
+ // Gather all cli params
96
+ const cliParams = {
97
+ env,
98
+ loglevel,
99
+ port,
100
+ adminport,
101
+ secured,
102
+ noadmin
103
+ }
104
+
105
+ // Start a server
106
+ let server = new DesignerServer(projectFlowFullPath, true, cliParams);
107
+ server.listen();
108
+ if (!noadmin) {
109
+ server.listenAdminServer();
110
+ }
111
+
112
+ // opn("http://localhost:1880/hello");
113
+ } catch (e) {
114
+ console.log(e);
115
+ logFatal(e.message);
116
+
117
+ }
118
+ };
119
+
120
+ /**
121
+ * Resolve a given path to a new path that points to the project flow json file.
122
+ * Scenarios:
123
+ * - If path undefined will resolve using current directory
124
+ * - If path directory, it should look up for a valid flow.json file, if it does not exist create it
125
+ * - If path is pointing to an actual flowFile, just return it.
126
+ *
127
+ * @param {string} projectDir
128
+ */
129
+ function resolveProjectFlowPath(projectDir) {
130
+ // output
131
+ let projectFlowFullPath;
132
+ let projectFlowDirname;
133
+
134
+ let projectDirOrFile = projectDir || process.cwd();
135
+
136
+ let isDir = isDirectory(projectDirOrFile);
137
+ if (isDir) {
138
+ let flowFileName = codegen.findFlowFile(projectDirOrFile); // returns only the flowname
139
+
140
+ if (!flowFileName) {
141
+ logError(`No flow found in directory: ${projectDirOrFile}`);
142
+ process.exit(1);
143
+ } else {
144
+ projectFlowDirname = projectDirOrFile;
145
+ projectFlowFullPath = path.join(projectDirOrFile, flowFileName);
146
+ }
147
+ } else if (isDir === false) {
148
+ projectFlowDirname = path.dirname(projectDirOrFile);
149
+ projectFlowFullPath = projectDirOrFile;
150
+ } else {
151
+ logError(`Directory does not exist: ${project_directory}`);
152
+ process.exit(1);
153
+ }
154
+
155
+ return [projectFlowDirname, projectFlowFullPath];
156
+ }
157
+
158
+ function isDirectory(dir) {
159
+ try {
160
+ let stats = fs.statSync(dir);
161
+ return stats.isDirectory();
162
+ } catch (err) {
163
+ return undefined;
164
+ }
165
+ }
@@ -0,0 +1,76 @@
1
+ const { performance } = require('perf_hooks');
2
+ const { InMemoryReporter, TerminalReporter } = require("../../../src/api/tools/test/lib/reporters");
3
+ const { TestCaseRunner } = require('../../../src/api/tools/test/lib/TestCaseRunner');
4
+ const { logError, logNotice, logInfo, logFatal } = require('../../utils/logger');
5
+
6
+
7
+ class TestSuiteRunner {
8
+ constructor(designerServer) {
9
+ this.designerServer = designerServer;
10
+ this.flowServer = this.designerServer.getFlowServer();
11
+ }
12
+ /**
13
+ *
14
+ * @param {*} testcases
15
+ * return an array containing found errors during execution, or undefined if successful
16
+ */
17
+ async runAll(testcases) {
18
+ logInfo(`TestCases - Executing\n`);
19
+ let testSuiteTimeStart = performance.now();
20
+
21
+ let totalPassedTestCases = 0;
22
+ let totalFailedCases = 0;
23
+ const totalTestCases = testcases.length;
24
+ for (let i = 0; i <= totalTestCases - 1; i++) {
25
+ let success = await this.runTestCase(testcases[i]);
26
+ if (success) {
27
+ totalPassedTestCases = totalPassedTestCases + 1;
28
+ } else {
29
+ totalFailedCases = totalFailedCases + 1;
30
+ }
31
+ }
32
+ let testSuiteTimeEnd = performance.now();
33
+ let testSuiteExecutionTimeInMs = testSuiteTimeEnd - testSuiteTimeStart;
34
+ this.printSummary(testSuiteExecutionTimeInMs, totalTestCases, totalFailedCases, totalPassedTestCases);
35
+ return 0;
36
+ }
37
+
38
+ /**
39
+ *
40
+ * @param {*} testcase
41
+ * @returns true if success, otherwise false
42
+ */
43
+ async runTestCase(testcase) {
44
+ let testcaseid = testcase.id;
45
+
46
+ const inMemReporter = new InMemoryReporter;
47
+ const terminalReporter = new TerminalReporter;
48
+
49
+ const testcaseRunner = new TestCaseRunner(this.flowServer, testcaseid, [inMemReporter, terminalReporter]);
50
+ try {
51
+ await testcaseRunner.runAsync();
52
+ return !inMemReporter.isFailedStatus()
53
+ } catch (err) {
54
+ console.log(err);
55
+ logFatal(`Unexpected error occurred while running testcase: "${testcase.name}" due to: ${err.message}.`)
56
+ }
57
+ }
58
+
59
+ printSummary(totalExecutionTimeInMs, totalTestCases, totalFailedCases, totalPassedTestCases) {
60
+ logInfo(`TestCases - Completed
61
+
62
+ Final Execution Statistics
63
+ -------------------------------
64
+ Execution Time: ${Math.floor(totalExecutionTimeInMs)} ms
65
+ Total Test Cases: ${totalTestCases}
66
+ Failed: ${totalFailedCases}
67
+ Passed: ${totalPassedTestCases}
68
+ -------------------------------
69
+ `);
70
+ }
71
+
72
+ }
73
+
74
+ module.exports = {
75
+ TestSuiteRunner
76
+ }
@@ -0,0 +1,123 @@
1
+ const path = require('path');
2
+ const { Select } = require('enquirer');
3
+ const wcmatch = require('wildcard-match');
4
+ const { util } = require('@kumologica/runtime');
5
+ const { codegen } = require('@kumologica/builder');
6
+
7
+ const { TestSuiteRunner } = require('./test/TestSuiteRunner');
8
+ const { DesignerServer } = require('../../src/server/DesignerServer');
9
+ const { logError, logNotice, logInfo, logFatal } = require('../utils/logger');
10
+
11
+ const isDirectory = util.isDirectorySync;
12
+
13
+
14
+ exports.command = 'test [project_directory]';
15
+ exports.desc = `Run test suite`;
16
+ exports.builder = (yargs) => {
17
+ yargs.positional(`project_directory`, {
18
+ type: 'string',
19
+ describe: 'Path to a valid kumologica project directory or flow file. (Optional)'
20
+ })
21
+ yargs.option(`testcase`, {
22
+ describe: "Testcase name to run",
23
+ type: 'string',
24
+ alias: 't',
25
+ nargs: 1
26
+ });
27
+ yargs.option(`iterative`, {
28
+ describe: "Manually select the testcase to run from all available testcases",
29
+ type: 'boolean',
30
+ alias: 'i'
31
+ });
32
+ }
33
+ exports.handler = ({ project_directory, testcase, iterative }) => {
34
+ let projectDirOrFile = project_directory || process.cwd();
35
+ let projectFlowPath = projectDirOrFile;
36
+
37
+ let isDir = isDirectory(projectDirOrFile);
38
+ if (isDir) {
39
+ let flowFileName = codegen.findFlowFile(projectDirOrFile); // returns only the flowname
40
+ if (!flowFileName) {
41
+ logFatal(`No flow found in directory: ${projectDirOrFile}`);
42
+ } else {
43
+ projectFlowPath = path.join(projectDirOrFile, flowFileName);
44
+ }
45
+ } else if (isDir === false) {
46
+ // do nothing as it was assumed to be a file
47
+ } else {
48
+ logFatal(`Directory does not exist: ${project_directory}`);
49
+ }
50
+
51
+ try {
52
+ runTestOnNewServer(projectFlowPath, testcase, iterative);
53
+ } catch (e) {
54
+ logFatal(e.message);
55
+ }
56
+ }
57
+
58
+ async function runTestOnNewServer(flowFilePath, testcaseSelected, iterative) {
59
+ let designerServer = new DesignerServer(
60
+ flowFilePath,
61
+ false,
62
+ {
63
+ loglevel: "error",
64
+ noadmin: true
65
+ });
66
+
67
+ try {
68
+ await designerServer.listen();
69
+ logInfo(`> Flow file: ${path.resolve(flowFilePath)} \n`);
70
+ let testSuiteRunner = new TestSuiteRunner(designerServer);
71
+
72
+ // If testcase is null, default to universal wildcard
73
+ testcaseSelected = testcaseSelected || "**";
74
+
75
+ // Find out all testcases available on the flow
76
+ let testcasesAvailable = codegen.findTestCasesFromFlow(flowFilePath);
77
+ if (!testcasesAvailable || (testcasesAvailable && testcasesAvailable.length === 0)) {
78
+ logFatal(`No testcases found on flow file: ${flowFileAbsPath}`);
79
+ };
80
+ let testcaseAvailableNames = testcasesAvailable.map(tc => tc.name);
81
+
82
+ // Capture the testcase from user on iterative mode
83
+ if (iterative) {
84
+ const prompt = new Select({
85
+ name: 'testcase',
86
+ message: 'What testcase do you want to run?',
87
+ choices: [...testcaseAvailableNames, 'Run all TestCases...']
88
+ });
89
+ await prompt.run()
90
+ .then(tc => {
91
+ if (tc === 'Run all TestCases...') {
92
+ testcaseSelected = "**";
93
+ } else {
94
+ testcaseSelected = tc;
95
+ }
96
+ })
97
+ .catch(err => {
98
+ logFatal(`Error found while running tests on iterative mode due to: `, err.message);
99
+ });
100
+ }
101
+
102
+ // Filter all testcases to be part of the test suite
103
+ const isMatch = wcmatch(testcaseSelected);
104
+ let testCasesSelected = [];
105
+ testcasesAvailable.forEach(async tc => {
106
+ if (isMatch(tc.name)) {
107
+ testCasesSelected.push({ name: tc.name, id: tc.id });
108
+ }
109
+ });
110
+
111
+ // Execute the testcasesIds if any, otherwise throw an error
112
+ if (testCasesSelected.length === 0) {
113
+ logFatal(`No matched testcases found`);
114
+ } else {
115
+ const errors = await testSuiteRunner.runAll(testCasesSelected);
116
+ process.exit(errors > 0);
117
+ }
118
+
119
+ } catch (err) {
120
+ console.log(err);
121
+ logFatal(`Unexpected error occurred while starting server due to <${err.message}>`);
122
+ }
123
+ }
@@ -0,0 +1,346 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const URL = require('url');
5
+ const BbPromise = require('bluebird');
6
+ const fse = require('fs-extra');
7
+ const qs = require('querystring');
8
+ const spawn = require('child-process-ext/spawn');
9
+ const renameService = require('./rename-service').renameService;
10
+ const KumologicaError = require('../KumologicaError');
11
+ const copyDirContentsSync = require('./fs/copy-dir-contents-sync');
12
+ const dirExistsSync = require('./fs/dir-exists-sync');
13
+ const walkDirSync = require('./fs/walk-dir-sync');
14
+ const { getTmpDirPath, getBaseTmpDirPath } = require('./fs/get-tmp-dir-path');
15
+
16
+
17
+ const OFFICAL_TEMPLATES_REPO = 'https://github.com/KumologicaHQ/kumologica-templates.git';
18
+ /**
19
+ * Returns directory path
20
+ * @param {Number} length
21
+ * @param {Array} parts
22
+ * @returns {String} directory path
23
+ */
24
+ function getPathDirectory(length, parts) {
25
+ if (!parts) {
26
+ return '';
27
+ }
28
+ return parts.slice(length).filter(Boolean).join(path.sep);
29
+ }
30
+
31
+ /**
32
+ * Validates URL
33
+ * @param {Object} url
34
+ * @param {String} hostname
35
+ * @param {String} service
36
+ * @param {String} owner
37
+ * @param {String} repo
38
+ */
39
+ function validateUrl({ url, hostname, service, owner, repo }) {
40
+ // validate if given url is a valid url
41
+ if (url.hostname !== hostname || !owner || !repo) {
42
+ const errorMessage = `The URL must be a valid ${service} URL in the following format: https://${hostname}/serverless/serverless`;
43
+ throw new KumologicaError(errorMessage, 'INVALID_TEMPLATE_URL');
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Check if the URL is pointing to a Git repository
49
+ * @param {String} url
50
+ */
51
+ function isPlainGitURL(url) {
52
+ return (url.startsWith('https') || url.startsWith('git@')) && url.endsWith('.git');
53
+ }
54
+
55
+ /**
56
+ * @param {Object} url
57
+ * @returns {Object}
58
+ */
59
+ function parseGitHubURL(url) {
60
+ const pathLength = 4;
61
+ const parts = url.pathname.split('/');
62
+ const isSubdirectory = parts.length > pathLength;
63
+ const owner = parts[1];
64
+ const repo = parts[2];
65
+ const branch = isSubdirectory ? parts[pathLength] : 'master';
66
+ const isGitHubEnterprise = url.hostname !== 'github.com';
67
+
68
+ if (!isGitHubEnterprise) {
69
+ // validate if given url is a valid GitHub url
70
+ validateUrl({ url, hostname: 'github.com', service: 'GitHub', owner, repo });
71
+ }
72
+
73
+ const downloadUrl = `https://${
74
+ isGitHubEnterprise ? url.hostname : 'github.com'
75
+ }/${owner}/${repo}/archive/${branch}.zip`;
76
+
77
+ return {
78
+ owner,
79
+ repo,
80
+ branch,
81
+ downloadUrl,
82
+ isSubdirectory,
83
+ pathToDirectory: getPathDirectory(pathLength + 1, parts),
84
+ username: url.username || '',
85
+ password: url.password || '',
86
+ };
87
+ }
88
+
89
+ /**
90
+ * @param {Object} url
91
+ * @returns {Object}
92
+ */
93
+ function parseBitbucketURL(url) {
94
+ const pathLength = 4;
95
+ const parts = url.pathname.split('/');
96
+ const isSubdirectory = parts.length > pathLength;
97
+ const owner = parts[1];
98
+ const repo = parts[2];
99
+
100
+ const query = qs.parse(url.query);
101
+ const branch = 'at' in query ? query.at : 'master';
102
+
103
+ // validate if given url is a valid Bitbucket url
104
+ validateUrl({ url, hostname: 'bitbucket.org', service: 'Bitbucket', owner, repo });
105
+
106
+ const downloadUrl = `https://bitbucket.org/${owner}/${repo}/get/${branch}.zip`;
107
+
108
+ return {
109
+ owner,
110
+ repo,
111
+ branch,
112
+ downloadUrl,
113
+ isSubdirectory,
114
+ pathToDirectory: getPathDirectory(pathLength + 1, parts),
115
+ username: url.username || '',
116
+ password: url.password || '',
117
+ };
118
+ }
119
+
120
+ function parseBitbucketServerURL(url) {
121
+ const pathLength = 9;
122
+ const parts = url.pathname.split('/');
123
+ const isSubdirectory = parts.length > pathLength;
124
+ const owner = parts[5];
125
+ const repo = parts[7];
126
+
127
+ const query = qs.parse(url.query);
128
+ const branch = 'at' in query ? decodeURIComponent(query.at) : 'master';
129
+
130
+ const downloadUrl = `${url.protocol}//${url.hostname}/rest/api/latest/projects/${owner}/repos/${repo}/archive${url.search}&format=zip`;
131
+
132
+ return {
133
+ owner,
134
+ repo,
135
+ branch,
136
+ downloadUrl,
137
+ isSubdirectory,
138
+ pathToDirectory: getPathDirectory(pathLength + 1, parts),
139
+ username: url.username || '',
140
+ password: url.password || '',
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Call `/rest/api/1.0/application-properties` to retrieve server info
146
+ * @param {Object} url
147
+ * @returns {Boolean}
148
+ */
149
+ async function retrieveBitbucketServerInfo(url) {
150
+ let requestOpts = {
151
+ url: `${url.protocol}//${url.hostname}/rest/api/1.0/application-properties`,
152
+ method: 'GET',
153
+ throwHttpErrors: false,
154
+ retry: 0
155
+ }
156
+ try {
157
+ let resp = await got(requestOpts)
158
+ return resp.body.displayName === 'Bitbucket';
159
+ }catch(err){
160
+ throw err;
161
+ }
162
+
163
+ }
164
+
165
+ /**
166
+ * @param {Object} url
167
+ * @returns {Object}
168
+ */
169
+ function parseGitlabURL(url) {
170
+ const pathLength = 4;
171
+ const parts = url.pathname.split('/');
172
+ const isSubdirectory = parts.length > pathLength;
173
+ const owner = parts[1];
174
+ const repo = parts[2];
175
+
176
+ const branch = isSubdirectory ? parts[pathLength] : 'master';
177
+
178
+ // validate if given url is a valid GitLab url
179
+ validateUrl({ url, hostname: 'gitlab.com', service: 'Bitbucket', owner, repo });
180
+
181
+ const downloadUrl = `https://gitlab.com/${owner}/${repo}/-/archive/${branch}/${repo}-${branch}.zip`;
182
+
183
+ return {
184
+ owner,
185
+ repo,
186
+ branch,
187
+ downloadUrl,
188
+ isSubdirectory,
189
+ pathToDirectory: getPathDirectory(pathLength + 1, parts),
190
+ username: url.username || '',
191
+ password: url.password || '',
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Parses a URL which points to a plain Git repository
197
+ * such as https://example.com/jdoe/project.git
198
+ *
199
+ * @param {String} url
200
+ * @returns {Object}
201
+ */
202
+ function parsePlainGitURL(url) {
203
+ const branch = 'master';
204
+ const downloadUrl = url;
205
+ const isSubdirectory = false;
206
+ const repo = url.match(/.+\/(.+)\.git/)[1];
207
+ return {
208
+ repo,
209
+ branch,
210
+ downloadUrl,
211
+ isSubdirectory,
212
+ username: url.username || '',
213
+ password: url.password || '',
214
+ };
215
+ }
216
+
217
+ /**
218
+ * Parse URL and call the appropriate adaptor
219
+ *
220
+ * @param {string} inputUrl
221
+ * @throws {KumologicaError}
222
+ * @returns {Promise}
223
+ */
224
+ async function parseRepoURL(inputUrl) {
225
+ return new BbPromise((resolve, reject) => {
226
+ if (!inputUrl) {
227
+ return reject(new KumologicaError('URL is required', 'MISSING_TEMPLATE_URL'));
228
+ }
229
+
230
+ const url = URL.parse(inputUrl.replace(/\/$/, ''));
231
+ if (url.auth) {
232
+ const [username, password] = url.auth.split(':');
233
+ url.username = username;
234
+ url.password = password;
235
+ }
236
+
237
+ // check if url parameter is a valid url
238
+ if (!url.host && !url.href.startsWith('git@')) {
239
+ return reject(new KumologicaError('The URL you passed is not valid', 'INVALID_TEMPLATE_URL'));
240
+ }
241
+
242
+ if (isPlainGitURL(url.href)) {
243
+ return resolve(parsePlainGitURL(inputUrl));
244
+ } else if (url.hostname === 'github.com' || url.hostname.indexOf('github.') !== -1) {
245
+ return resolve(parseGitHubURL(url));
246
+ } else if (url.hostname === 'bitbucket.org') {
247
+ return resolve(parseBitbucketURL(url));
248
+ } else if (url.hostname === 'gitlab.com') {
249
+ return resolve(parseGitlabURL(url));
250
+ }
251
+
252
+ const msg =
253
+ 'The URL you passed is not one of the valid providers: "GitHub", "GitHub Entreprise", "Bitbucket", "Bitbucket Server" or "GitLab".';
254
+ const err = new KumologicaError(msg, 'INVALID_TEMPLATE_PROVIDER');
255
+ // test if it's a private bitbucket server
256
+ return retrieveBitbucketServerInfo(url)
257
+ .then((isBitbucket) => {
258
+ if (!isBitbucket) {
259
+ return reject(err);
260
+ }
261
+
262
+ // build download URL
263
+ return resolve(parseBitbucketServerURL(url));
264
+ })
265
+ .catch(() => reject(err));
266
+ });
267
+ }
268
+
269
+ /**
270
+ * Downloads the content of the template into the projectName directory
271
+ * @param {string} [repoUrl] - the git repository
272
+ * @param {string} [templateName] - the name of the project (optional), in case we are only interested in a particular directory within the repo
273
+ * @param {string} projectName - the local project name / directory to be created
274
+ * @returns {Promise}
275
+ */
276
+ async function downloadTemplateFromRepo(repoUrl, templateName, projectPath, projectName) {
277
+ try{
278
+ repoUrl = repoUrl || OFFICAL_TEMPLATES_REPO;
279
+ const repoInformation = await parseRepoURL(repoUrl);
280
+
281
+ const tempBaseDirectory = getBaseTmpDirPath();
282
+ // clean up temp directory
283
+ fse.removeSync(tempBaseDirectory);
284
+ const tempRepoDirectory = getTmpDirPath();
285
+ // const { username, password } = repoInformation;
286
+
287
+ const renamed = templateName !== projectName;
288
+
289
+ // Source project directory
290
+ const srcProjectDir = templateName? path.join(tempRepoDirectory, templateName) : tempRepoDirectory;
291
+
292
+ // Target project directory
293
+ const targetProjectDir = path.join(projectPath, projectName);
294
+
295
+ if (dirExistsSync(targetProjectDir)) {
296
+ const errorMessage = `A project already exist in path: "${targetProjectDir}".`;
297
+ throw new KumologicaError(errorMessage, 'TARGET_FOLDER_ALREADY_EXISTS');
298
+ }
299
+
300
+ if (isPlainGitURL(repoUrl)) {
301
+ return spawn('git', ['clone', repoUrl, tempRepoDirectory]).then(() => {
302
+ try {
303
+ copyDirContentsSync(srcProjectDir, targetProjectDir);
304
+ fse.removeSync(tempBaseDirectory);
305
+ if (renamed) renameService(targetProjectDir, projectName);
306
+ }catch(err){
307
+ throw new KumologicaError(`Template name: "${templateName}" does not exist. Run "kl list-templates" to see list of available templates.`);
308
+ }
309
+ return "";
310
+ });
311
+ } else {
312
+ throw new KumologicaError(`Git repo URL is incorrect`);
313
+ }
314
+ }catch(err){
315
+ throw new KumologicaError(err.message);
316
+ }
317
+ }
318
+
319
+ async function listAvailableTemplates() {
320
+ try{
321
+ const tempBaseDirectory = getBaseTmpDirPath();
322
+ const tempRepoDirectory = getTmpDirPath();
323
+
324
+ await parseRepoURL(OFFICAL_TEMPLATES_REPO);
325
+
326
+ const listTemplates = spawn('git', ['clone', OFFICAL_TEMPLATES_REPO, tempRepoDirectory]).then(() => {
327
+ try {
328
+ const projects = walkDirSync(tempRepoDirectory);
329
+ fse.removeSync(tempBaseDirectory);
330
+ return projects;
331
+ }catch(err){
332
+ throw new KumologicaError(err.message);
333
+ }
334
+ });
335
+ // ListTemplates: ["t1", "t2"]
336
+ return listTemplates;
337
+ } catch(err){
338
+ throw new KumologicaError(err.message);
339
+ }
340
+ }
341
+
342
+ module.exports = {
343
+ downloadTemplateFromRepo,
344
+ listAvailableTemplates,
345
+ isPlainGitURL
346
+ };