contensis-cli 1.1.1 → 1.1.2-beta.1

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 (47) hide show
  1. package/dist/factories/RequestHandlerFactory.js +203 -0
  2. package/dist/factories/RequestHandlerFactory.js.map +7 -0
  3. package/dist/localisation/en-GB.js +28 -5
  4. package/dist/localisation/en-GB.js.map +2 -2
  5. package/dist/mappers/DevRequests-to-RequestHanderCliArgs.js +119 -0
  6. package/dist/mappers/DevRequests-to-RequestHanderCliArgs.js.map +7 -0
  7. package/dist/providers/GitHubCliModuleProvider.js +107 -0
  8. package/dist/providers/GitHubCliModuleProvider.js.map +7 -0
  9. package/dist/providers/HttpProvider.js +72 -0
  10. package/dist/providers/HttpProvider.js.map +7 -0
  11. package/dist/providers/ManifestProvider.js +53 -0
  12. package/dist/providers/ManifestProvider.js.map +7 -0
  13. package/dist/providers/file-provider.js +11 -0
  14. package/dist/providers/file-provider.js.map +2 -2
  15. package/dist/services/ContensisAuthService.js +19 -11
  16. package/dist/services/ContensisAuthService.js.map +2 -2
  17. package/dist/services/ContensisCliService.js +27 -14
  18. package/dist/services/ContensisCliService.js.map +2 -2
  19. package/dist/services/ContensisDevService.js +10 -59
  20. package/dist/services/ContensisDevService.js.map +3 -3
  21. package/dist/util/debug.js +29 -0
  22. package/dist/util/debug.js.map +7 -0
  23. package/dist/util/fetch.js +65 -0
  24. package/dist/util/fetch.js.map +7 -0
  25. package/dist/util/index.js.map +1 -1
  26. package/dist/util/logger.js.map +2 -2
  27. package/dist/version.js +1 -1
  28. package/dist/version.js.map +1 -1
  29. package/package.json +4 -1
  30. package/src/factories/RequestHandlerFactory.ts +225 -0
  31. package/src/localisation/en-GB.ts +54 -12
  32. package/src/mappers/DevRequests-to-RequestHanderCliArgs.ts +145 -0
  33. package/src/providers/GitHubCliModuleProvider.ts +114 -0
  34. package/src/providers/HttpProvider.ts +50 -0
  35. package/src/providers/ManifestProvider.ts +43 -0
  36. package/src/providers/file-provider.ts +9 -0
  37. package/src/services/ContensisAuthService.ts +23 -14
  38. package/src/services/ContensisCliService.ts +30 -15
  39. package/src/services/ContensisDevService.ts +19 -85
  40. package/src/util/debug.ts +1 -0
  41. package/src/util/fetch.ts +74 -0
  42. package/src/util/index.ts +1 -1
  43. package/src/util/logger.ts +0 -1
  44. package/src/version.ts +1 -1
  45. package/dist/mappers/DevRequests-to-RequestHanderSiteConfigYaml.js +0 -56
  46. package/dist/mappers/DevRequests-to-RequestHanderSiteConfigYaml.js.map +0 -7
  47. package/src/mappers/DevRequests-to-RequestHanderSiteConfigYaml.ts +0 -44
@@ -0,0 +1,225 @@
1
+ import { spawn } from 'child_process';
2
+ import inquirer from 'inquirer';
3
+ import { createSpinner } from 'nanospinner';
4
+ import path from 'path';
5
+ import { LogMessages } from '~/localisation/en-GB';
6
+ import GitHubCliModuleProvider from '~/providers/GitHubCliModuleProvider';
7
+
8
+ import ManifestProvider from '~/providers/ManifestProvider';
9
+ import { appRootDir, joinPath } from '~/providers/file-provider';
10
+ import { isDebug } from '~/util/debug';
11
+ import { Logger } from '~/util/logger';
12
+
13
+ export class RequestHandlerFactory {
14
+ debug = isDebug();
15
+ log = Logger;
16
+ messages = LogMessages;
17
+ manifest = new ManifestProvider(); // Load cli-manifest.json
18
+
19
+ basePath = path.join(appRootDir);
20
+ name = 'request-handler-localdevelopment';
21
+ cmd = 'Zengenti.Contensis.RequestHandler.LocalDevelopment';
22
+
23
+ prerelease;
24
+
25
+ get exePath() {
26
+ return path.join(this.basePath, `${this.name}-${this.moduleInfo.version}`);
27
+ }
28
+
29
+ get moduleInfo() {
30
+ return (
31
+ this.manifest.getModule(this.name) || {
32
+ github: 'contensis/request-handler-localdevelopment',
33
+ version: '*',
34
+ }
35
+ );
36
+ }
37
+
38
+ constructor(prerelease = false) {
39
+ this.prerelease = prerelease;
40
+ }
41
+
42
+ // Use the factory to create a request handler instance
43
+ // handling the download and updating of the external binary
44
+ async Create() {
45
+ const { moduleInfo } = this;
46
+ const firstUse = !moduleInfo?.version || moduleInfo?.version === '*';
47
+
48
+ if (firstUse) {
49
+ // Create cli-manifest.json
50
+ this.manifest.writeModule(this.name, this.moduleInfo);
51
+
52
+ // Download for first time use (await)
53
+ await this.CheckUpdate({ verbose: true });
54
+ }
55
+
56
+ // Apply any downloaded/pending update so we launch that version
57
+ await this.ApplyUpdate();
58
+
59
+ // Fire an async update check and continue working in the background (do not await)
60
+ if (!firstUse) this.CheckUpdate();
61
+
62
+ // Return a RequestHandler ready to invoke
63
+ return this.CreateInvoke(this);
64
+ }
65
+
66
+ CreateInvoke(self = this) {
67
+ // Hoist the vars we need from `this` as we lose scope
68
+ // when the function is returned from the Create() method
69
+ const { debug, log, messages, cmd, exePath } = self;
70
+
71
+ // Invoke request handler method
72
+ return async (args: string[]) => {
73
+ const child = spawn(joinPath(exePath, cmd), args, { stdio: 'inherit' });
74
+
75
+ if (args?.length && debug)
76
+ log.warning(
77
+ `Spawning process with supplied args: ${JSON.stringify(
78
+ child.spawnargs,
79
+ null,
80
+ 2
81
+ )}`
82
+ );
83
+
84
+ let isRunning = false;
85
+
86
+ // Log child output through event listeners
87
+ child?.stdout?.on('data', data => {
88
+ isRunning = true;
89
+ log.raw(data);
90
+ });
91
+
92
+ child?.stderr?.on('data', data => {
93
+ log.error(data);
94
+ });
95
+
96
+ child.on('spawn', () => {
97
+ isRunning = true;
98
+ log.help(messages.devrequests.spawn());
99
+ });
100
+
101
+ child.on('exit', code => {
102
+ isRunning = false;
103
+
104
+ log[code === 0 ? 'success' : 'warning'](
105
+ messages.devrequests.exited(code)
106
+ );
107
+ });
108
+
109
+ child.on('error', error => {
110
+ isRunning = false;
111
+ log.error(messages.devrequests.errored(error));
112
+ });
113
+
114
+ await new Promise(resolve => setTimeout(resolve, 2000));
115
+
116
+ // keep the method running until we can return
117
+ while (true === true) {
118
+ if (!isRunning) return;
119
+ await new Promise(resolve => setTimeout(resolve, 1000));
120
+ }
121
+ };
122
+ }
123
+
124
+ async CheckUpdate({ verbose = false }: { verbose?: boolean } = {}) {
125
+ const { debug, log, manifest, messages, moduleInfo } = this;
126
+ const github = new GitHubCliModuleProvider(moduleInfo.github);
127
+ // Find latest version
128
+ const release = await github.FindLatestRelease();
129
+ if (verbose || debug)
130
+ if (release)
131
+ log.info(
132
+ `${messages.devrequests.install.download(
133
+ moduleInfo.github,
134
+ release.tag_name
135
+ )}\n${release.html_url}`
136
+ );
137
+ else
138
+ log.warning(messages.devrequests.install.notFound(moduleInfo.github));
139
+
140
+ // Should we download an update?
141
+ if (
142
+ release?.tag_name &&
143
+ ![moduleInfo.version, moduleInfo.install].includes(release.tag_name)
144
+ ) {
145
+ // Download platform-specific release asset
146
+ const downloadPath = path.join(
147
+ this.basePath,
148
+ `${this.name}-${release.tag_name}`
149
+ );
150
+
151
+ // add spinner while downloading
152
+ const spinner = createSpinner(
153
+ messages.devrequests.install.downloading(
154
+ moduleInfo.github,
155
+ release.tag_name
156
+ )
157
+ );
158
+ if (verbose || debug) {
159
+ spinner.start();
160
+ }
161
+ try {
162
+ await github.DownloadRelease(release, {
163
+ path: downloadPath,
164
+ // Map NodeJS os platform to release asset name
165
+ platforms: [
166
+ ['win32', 'win-x64'],
167
+ ['darwin', 'osx-x64'],
168
+ ['linux', 'linux-x64'],
169
+ ],
170
+ });
171
+ } catch (ex: any) {
172
+ spinner.error();
173
+ log.error(
174
+ messages.devrequests.install.downloadFail(
175
+ moduleInfo.github,
176
+ release.tag_name
177
+ ),
178
+ ex
179
+ );
180
+ } finally {
181
+ if (verbose || debug)
182
+ spinner.success({
183
+ text: messages.devrequests.install.downloaded(
184
+ moduleInfo.github,
185
+ release.tag_name
186
+ ),
187
+ });
188
+
189
+ // Update module info with downloaded release
190
+ this.moduleInfo.install = release.tag_name;
191
+ // Write module info update to manifest so it installs on next invoke
192
+ manifest.writeModule(this.name, this.moduleInfo);
193
+ }
194
+ }
195
+ }
196
+
197
+ async ApplyUpdate() {
198
+ const { manifest, messages, moduleInfo } = this;
199
+
200
+ if (moduleInfo.install && moduleInfo.version !== moduleInfo.install) {
201
+ let { apply } =
202
+ moduleInfo.version === '*'
203
+ ? { apply: true }
204
+ : await inquirer.prompt({
205
+ name: 'apply',
206
+ type: 'confirm',
207
+ message: messages.devrequests.install.applyUpdate(
208
+ moduleInfo.install,
209
+ moduleInfo.version
210
+ ),
211
+ default: 'Y',
212
+ });
213
+
214
+ if (apply) {
215
+ moduleInfo.version = moduleInfo.install;
216
+ delete moduleInfo.install;
217
+ manifest.writeModule(this.name, this.moduleInfo);
218
+
219
+ // TODO: clean up user folder by deleting old version(s)}
220
+ }
221
+ }
222
+ }
223
+ }
224
+
225
+ export const createRequestHandler = () => new RequestHandlerFactory().Create();
@@ -341,10 +341,36 @@ export const LogMessages = {
341
341
  return Logger.infoText(status);
342
342
  }
343
343
  },
344
- get: (id: string, env: string, projectId?: string) =>
345
- `[${env}] Block ${id} in project ${projectId}:`,
344
+ get: (
345
+ id: string,
346
+ branch: string,
347
+ version: string,
348
+ env: string,
349
+ projectId?: string
350
+ ) =>
351
+ `[${env}] ${
352
+ version && version !== 'latest'
353
+ ? `Found v${version}`
354
+ : 'Latest block versions'
355
+ } for ${Logger.infoText(`${branch}/`)}${Logger.highlightText(
356
+ id
357
+ )} in project ${projectId}\n`,
358
+ noGet: (
359
+ id: string,
360
+ branch: string,
361
+ version: string,
362
+ env: string,
363
+ projectId?: string
364
+ ) =>
365
+ `[${env}] Did not find ${
366
+ version ? `v${version}` : 'any block versions'
367
+ } for ${Logger.highlightText(id)} in branch ${Logger.infoText(
368
+ branch
369
+ )} in ${Logger.infoText(projectId)} project`,
370
+ noGetTip: () =>
371
+ `Check the available blocks and branches in this project by running "list blocks"`,
346
372
  list: (env: string, projectId?: string) =>
347
- `[${env}] Blocks in project ${projectId}:`,
373
+ `[${env}] Blocks in project ${projectId}\n`,
348
374
  noList: (env: string, projectId?: string) =>
349
375
  `[${env}] Cannot retrieve blocks in project ${projectId}`,
350
376
  getLogs: (id: string, branch: string, env: string, projectId?: string) =>
@@ -367,15 +393,6 @@ export const LogMessages = {
367
393
  `[${env}] Unable to push block ${Logger.highlightText(
368
394
  id
369
395
  )} in project ${projectId}`,
370
- latestVersion: (
371
- version: string,
372
- id: string,
373
- env: string,
374
- projectId?: string
375
- ) =>
376
- `[${env}] Found latest block version ${Logger.highlightText(
377
- id
378
- )} in project ${projectId} ${Logger.highlightText(version)}`,
379
396
  failedParsingVersion: () =>
380
397
  `Did not find a "version.versionNo" in response`,
381
398
  actionComplete: (
@@ -586,4 +603,29 @@ export const LogMessages = {
586
603
  'my-awesome-website'
587
604
  )}`,
588
605
  },
606
+ devrequests: {
607
+ install: {
608
+ notFound: (repo: string) =>
609
+ `Could not find a release in ${repo} repo - please check github`,
610
+ download: (repo: string, version: string) =>
611
+ `Found release ${repo} ${version}`,
612
+ downloading: (repo: string, version: string) =>
613
+ `Downloading ${repo} ${version}`,
614
+ downloadFail: (repo: string, version: string) =>
615
+ `Problem downloading release ${version} from ${repo}`,
616
+ downloaded: (repo: string, version: string) =>
617
+ `Downloaded ${repo} ${version}`,
618
+ applyUpdate: (version: string, existing: string) =>
619
+ `Use updated version ${version} ${Logger.infoText(
620
+ `(replaces ${existing})`
621
+ )}?`,
622
+ },
623
+ launch: () => `Launching request handler for local development`,
624
+ spawn: () =>
625
+ `If you see a firewall popup requesting network access, it is safe to approve`,
626
+ exited: (code: number | null) =>
627
+ `Request handler exited with code ${code}\n`,
628
+ errored: (error: Error) =>
629
+ `Could not launch request handler due to error \n${error}`,
630
+ },
589
631
  };
@@ -0,0 +1,145 @@
1
+ import { ContensisMigrationService } from 'migratortron';
2
+ import ContensisCli from '~/services/ContensisCliService';
3
+
4
+ type EndpointJson = {
5
+ id: string;
6
+ path: string;
7
+ };
8
+
9
+ type BlockJson = {
10
+ id: string;
11
+ baseUri: string;
12
+ staticPaths: string[];
13
+ endpoints: EndpointJson[];
14
+ versionNo: number;
15
+ branch: string;
16
+ };
17
+
18
+ type RendererJson = {
19
+ id: string;
20
+ name: string;
21
+ rules: RendererRuleJson[];
22
+ assignedContentTypes: string[];
23
+ };
24
+
25
+ type RendererRuleJson = {
26
+ return?: {
27
+ blockId?: string;
28
+ endpointId?: string | null;
29
+ version?: string;
30
+ };
31
+ };
32
+ interface ISiteConfigYaml {
33
+ alias: string;
34
+ projectId: string;
35
+ accessToken: string;
36
+ clientId: string;
37
+ sharedSecret: string;
38
+ blocks: BlockJson[];
39
+ renderers: RendererJson[];
40
+ }
41
+
42
+ export const buildSiteConfig = async (cli: ContensisCli) => {
43
+ const { currentEnv, env, log, messages } = cli;
44
+ const siteConfig: ISiteConfigYaml = {
45
+ alias: cli.currentEnv,
46
+ projectId: cli.currentProject,
47
+ accessToken: '',
48
+ clientId: '',
49
+ sharedSecret: '',
50
+ blocks: [],
51
+ renderers: [],
52
+ };
53
+
54
+ const getBlocks = async (contensis: ContensisMigrationService) => {
55
+ const [err, blocksRaw] = await contensis.blocks.GetBlocks();
56
+ if (err) log.error(messages.blocks.noList(currentEnv, env.currentProject));
57
+
58
+ // const blocksRaw = await cli.PrintBlocks();
59
+
60
+ const blocks: BlockJson[] = [];
61
+ for (const block of blocksRaw || []) {
62
+ // Retrieve block version
63
+ const [err, versions] = await contensis.blocks.GetBlockVersions(
64
+ block.id,
65
+ 'default',
66
+ 'latest'
67
+ );
68
+ if (err || versions?.length === 0)
69
+ log.warning(
70
+ messages.blocks.noGet(
71
+ block.id,
72
+ 'default',
73
+ 'latest',
74
+ currentEnv,
75
+ env.currentProject
76
+ )
77
+ );
78
+ if (versions?.[0]) {
79
+ const v = versions[0];
80
+ blocks.push({
81
+ id: v.id,
82
+ baseUri: v.previewUrl,
83
+ staticPaths: v.staticPaths,
84
+ endpoints: v.endpoints,
85
+ versionNo: v.version.versionNo,
86
+ branch: v.source.branch,
87
+ });
88
+ }
89
+ }
90
+ return blocks;
91
+ };
92
+
93
+ const contensis = await cli.ConnectContensis();
94
+ if (contensis) {
95
+ const [blocks, renderers] = await Promise.all([
96
+ getBlocks(contensis),
97
+ contensis.renderers.GetRenderers(),
98
+ ]);
99
+
100
+ siteConfig.blocks = blocks;
101
+ siteConfig.renderers = renderers?.[1]?.map(r => ({
102
+ id: r.id,
103
+ name: r.name,
104
+ assignedContentTypes: r.assignedContentTypes,
105
+ rules: r.rules,
106
+ }));
107
+ }
108
+ return siteConfig;
109
+ };
110
+
111
+ export const requestHandlerCliArgs = async (
112
+ cli: ContensisCli,
113
+ overrideArgs: string[] = []
114
+ ) => {
115
+ const args = overrideArgs
116
+ ? typeof overrideArgs?.[0] === 'string' && overrideArgs[0].includes(' ', 2)
117
+ ? overrideArgs[0].split(' ')
118
+ : overrideArgs
119
+ : []; // args could be [ '-c .\\site_config.yaml' ] or [ '-c', '.\\site_config.yaml' ]
120
+
121
+ const siteConfig = await buildSiteConfig(cli);
122
+ // Add required args
123
+ if (!args.find(a => a === '--alias')) args.push('--alias', cli.currentEnv);
124
+ if (!args.find(a => a === '--project-api-id'))
125
+ args.push('--project-api-id', cli.currentProject);
126
+ if (!args.find(a => a === '--blocks-json'))
127
+ args.push('--blocks-json', JSON.stringify(siteConfig.blocks));
128
+ if (!args.find(a => a === '--renderers-json'))
129
+ args.push('--renderers-json', JSON.stringify(siteConfig.renderers));
130
+
131
+ await cli.Login(cli.env.lastUserId, { silent: true }); // to hydrate the auth service
132
+ const client = cli.auth?.clientDetails;
133
+ if (client) {
134
+ if (!args.find(a => a === '--client-id') && 'clientId' in client)
135
+ args.push('--client-id', client.clientId);
136
+ if (!args.find(a => a === '--client-secret') && 'clientSecret' in client)
137
+ args.push('--client-secret', client.clientSecret);
138
+ if (!args.find(a => a === '--username') && 'username' in client)
139
+ args.push('--username', client.username);
140
+ if (!args.find(a => a === '--password') && 'password' in client)
141
+ args.push('--password', client.password);
142
+ }
143
+
144
+ return args;
145
+ };
@@ -0,0 +1,114 @@
1
+ import os from 'os';
2
+ import Zip from 'adm-zip';
3
+ import type { Endpoints } from '@octokit/types';
4
+ import HttpProvider from '~/providers/HttpProvider';
5
+ import {
6
+ checkDir,
7
+ joinPath,
8
+ removeDirectory,
9
+ removeFile,
10
+ } from './file-provider';
11
+ import { doRetry } from '~/util/fetch';
12
+
13
+ type GitHubApiRelease =
14
+ Endpoints['GET /repos/{owner}/{repo}/releases/latest']['response']['data'];
15
+
16
+ class GitHubCliModuleProvider {
17
+ http: HttpProvider;
18
+ repo: string;
19
+ baseUrl = 'https://api.github.com/repos';
20
+
21
+ get releases_url() {
22
+ return `${this.baseUrl}/${this.repo}/releases`;
23
+ }
24
+ get latest_release_url() {
25
+ return `${this.baseUrl}/${this.repo}/releases/latest`;
26
+ }
27
+
28
+ download?: {
29
+ tag: string;
30
+ name: string;
31
+ url: string;
32
+ browser_url: string;
33
+ };
34
+
35
+ constructor(repo: string) {
36
+ this.http = new HttpProvider();
37
+ this.repo = repo;
38
+ }
39
+
40
+ async FindLatestRelease() {
41
+ const { http, latest_release_url, releases_url } = this;
42
+ // return latest tag version is:
43
+
44
+ const responses = await Promise.all([
45
+ http.get<GitHubApiRelease>(latest_release_url, {
46
+ doRetry: doRetry({ silent: true }),
47
+ }),
48
+ http.get<GitHubApiRelease[]>(releases_url),
49
+ ]);
50
+
51
+ const [latestErr, latest, latestResponse] = responses[0];
52
+ const [releasesErr, releases] = responses[1];
53
+
54
+ if (releasesErr) {
55
+ throw new Error(`Unable to get releases`, { cause: releasesErr });
56
+ } else if (!releases || releases.length === 0)
57
+ throw new Error(`No releases available`);
58
+ else if (latestErr && !latest) {
59
+ if (latestResponse?.status === 404 && releases?.length) {
60
+ // No latest release, check releases for prerelease version, fallback to last release
61
+ const release = releases.find(r => r.prerelease) || releases[0];
62
+
63
+ if (release) {
64
+ return release;
65
+ }
66
+ }
67
+ } else {
68
+ return latest;
69
+ }
70
+ }
71
+
72
+ async DownloadRelease(
73
+ release: GitHubApiRelease,
74
+ {
75
+ path,
76
+ platforms,
77
+ unzip = true,
78
+ }: { path: string; unzip?: boolean; platforms: [NodeJS.Platform, string][] }
79
+ ) {
80
+ // find os-specific asset
81
+ const platform = platforms.find(p => p[0] === os.platform()) || [
82
+ os.platform(),
83
+ os.platform(),
84
+ ];
85
+
86
+ const asset = release.assets.find(r =>
87
+ r.name.toLowerCase().includes(platform[1])
88
+ );
89
+
90
+ // download asset
91
+ if (asset) {
92
+ const filePath = joinPath(path, asset.name);
93
+ removeDirectory(path);
94
+ checkDir(filePath);
95
+ await this.http.downloadFile(asset.browser_download_url, filePath);
96
+
97
+ if (unzip && asset.name.endsWith('.zip')) {
98
+ // unzip the downloaded file
99
+ const zipFile = new Zip(filePath);
100
+ zipFile.extractAllTo(path);
101
+
102
+ // delete the downloaded zip file
103
+ removeFile(filePath);
104
+ }
105
+ } else
106
+ throw new Error(
107
+ `no asset found in release ${
108
+ release.tag_name
109
+ } for platform ${os.platform()}\n${release.html_url}`
110
+ );
111
+ }
112
+ }
113
+
114
+ export default GitHubCliModuleProvider;
@@ -0,0 +1,50 @@
1
+ import to from 'await-to-js';
2
+ import { FetchInit } from 'enterprise-fetch';
3
+ import fs from 'fs';
4
+ import { Readable } from 'stream';
5
+ import { finished } from 'stream/promises';
6
+
7
+ import { isJson, tryParse } from '~/util';
8
+ import { enhancedFetch } from '~/util/fetch';
9
+ class HttpProvider {
10
+ constructor() {}
11
+
12
+ async get<T = any>(url: string, init: FetchInit = {}) {
13
+ return this.fetch<T>(url, { method: 'GET', ...init });
14
+ }
15
+
16
+ async fetch<T = any>(
17
+ uri: string,
18
+ init: FetchInit = {}
19
+ ): Promise<[Error | null, T | undefined, Response | undefined]> {
20
+ const [error, response] = await to(enhancedFetch(uri, init));
21
+
22
+ if (response && !error) {
23
+ const [bodyError, text] = await to(response.text());
24
+ if (bodyError) return [bodyError, undefined, response];
25
+ if (isJson(text)) {
26
+ const err =
27
+ !response.status || !response.ok ? tryParse(text) : undefined;
28
+ const payload =
29
+ response.status && response.ok ? tryParse(text) : undefined;
30
+ return [err, payload, response];
31
+ }
32
+ return [
33
+ response.ok ? null : new Error(text),
34
+ response.ok ? (text as unknown as T) : undefined,
35
+ response,
36
+ ];
37
+ }
38
+ return [error, undefined, response];
39
+ }
40
+
41
+ async downloadFile(url: string, destination: string) {
42
+ const res = await fetch(url);
43
+ if (res.ok && res.body !== null) {
44
+ const fileStream = fs.createWriteStream(destination, { flags: 'wx' });
45
+ await finished(Readable.fromWeb(res.body as any).pipe(fileStream));
46
+ }
47
+ }
48
+ }
49
+
50
+ export default HttpProvider;
@@ -0,0 +1,43 @@
1
+ import { tryParse } from '~/util';
2
+ import { appPath, readFile, writeFile } from './file-provider';
3
+
4
+ export type CliModule = {
5
+ github: string;
6
+ version: string;
7
+ install?: string;
8
+ cmd?: string;
9
+ };
10
+
11
+ type CliManifest = {
12
+ [moduleName: string]: CliModule;
13
+ };
14
+
15
+ const MANIFEST_PATH = appPath('cli-manifest.json');
16
+
17
+ class ManifestProvider {
18
+ private manifest: CliManifest;
19
+
20
+ constructor() {
21
+ const manifest = tryParse(readFile(MANIFEST_PATH));
22
+ this.manifest = manifest || {};
23
+ }
24
+
25
+ get() {
26
+ return this.manifest;
27
+ }
28
+
29
+ getModule(name: string) {
30
+ return this.manifest?.[name];
31
+ }
32
+ writeModule(name: string, moduleInfo: CliModule) {
33
+ if (this.manifest) this.manifest[name] = moduleInfo;
34
+ else
35
+ this.manifest = {
36
+ [name]: moduleInfo,
37
+ };
38
+
39
+ writeFile(MANIFEST_PATH, JSON.stringify(this.manifest, null, 2));
40
+ }
41
+ }
42
+
43
+ export default ManifestProvider;
@@ -50,6 +50,13 @@ export const removeFile = (filePath: string) => {
50
50
  }
51
51
  };
52
52
 
53
+ export const removeDirectory = (filePath: string) => {
54
+ const directoryPath = appPath(filePath);
55
+ if (fs.existsSync(directoryPath)) {
56
+ fs.rmSync(directoryPath, { force: true, recursive: true });
57
+ }
58
+ };
59
+
53
60
  export const moveFile = (file: string, fromPath: string, toPath: string) => {
54
61
  const from = path.join(appRootDir, `${fromPath}${file}`);
55
62
  const to = path.join(appRootDir, `${toPath}${file}`);
@@ -84,6 +91,8 @@ export const appPath = (filePath: string) =>
84
91
  export const cwdPath = (filePath: string) =>
85
92
  path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath);
86
93
 
94
+ export const joinPath = path.join;
95
+
87
96
  type DetectedFileType =
88
97
  | { type: 'json'; contents: any }
89
98
  | { type: 'xml' | 'csv'; contents: string };