@optimizely/ocp-cli 1.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +18 -0
  3. package/bin/opti.js +3 -0
  4. package/dist/commands/accounts/Whoami.d.ts +3 -0
  5. package/dist/commands/accounts/Whoami.js +34 -0
  6. package/dist/commands/accounts/Whoami.js.map +1 -0
  7. package/dist/commands/accounts/Whois.d.ts +6 -0
  8. package/dist/commands/accounts/Whois.js +65 -0
  9. package/dist/commands/accounts/Whois.js.map +1 -0
  10. package/dist/commands/app/BaseBuildCommand.d.ts +23 -0
  11. package/dist/commands/app/BaseBuildCommand.js +227 -0
  12. package/dist/commands/app/BaseBuildCommand.js.map +1 -0
  13. package/dist/commands/app/Init.d.ts +20 -0
  14. package/dist/commands/app/Init.js +285 -0
  15. package/dist/commands/app/Init.js.map +1 -0
  16. package/dist/commands/app/Logs.d.ts +19 -0
  17. package/dist/commands/app/Logs.js +230 -0
  18. package/dist/commands/app/Logs.js.map +1 -0
  19. package/dist/commands/app/Package.d.ts +4 -0
  20. package/dist/commands/app/Package.js +51 -0
  21. package/dist/commands/app/Package.js.map +1 -0
  22. package/dist/commands/app/Prepare.d.ts +8 -0
  23. package/dist/commands/app/Prepare.js +112 -0
  24. package/dist/commands/app/Prepare.js.map +1 -0
  25. package/dist/commands/app/Register.d.ts +7 -0
  26. package/dist/commands/app/Register.js +58 -0
  27. package/dist/commands/app/Register.js.map +1 -0
  28. package/dist/commands/app/Validate.d.ts +4 -0
  29. package/dist/commands/app/Validate.js +27 -0
  30. package/dist/commands/app/Validate.js.map +1 -0
  31. package/dist/commands/availability/List.d.ts +4 -0
  32. package/dist/commands/availability/List.js +47 -0
  33. package/dist/commands/availability/List.js.map +1 -0
  34. package/dist/commands/directory/Info.d.ts +7 -0
  35. package/dist/commands/directory/Info.js +92 -0
  36. package/dist/commands/directory/Info.js.map +1 -0
  37. package/dist/commands/directory/Install.d.ts +6 -0
  38. package/dist/commands/directory/Install.js +54 -0
  39. package/dist/commands/directory/Install.js.map +1 -0
  40. package/dist/commands/directory/List.d.ts +8 -0
  41. package/dist/commands/directory/List.js +102 -0
  42. package/dist/commands/directory/List.js.map +1 -0
  43. package/dist/commands/directory/ListFunctions.d.ts +7 -0
  44. package/dist/commands/directory/ListFunctions.js +77 -0
  45. package/dist/commands/directory/ListFunctions.js.map +1 -0
  46. package/dist/commands/directory/ListGlobalFunctions.d.ts +6 -0
  47. package/dist/commands/directory/ListGlobalFunctions.js +72 -0
  48. package/dist/commands/directory/ListGlobalFunctions.js.map +1 -0
  49. package/dist/commands/directory/ListInstalls.d.ts +8 -0
  50. package/dist/commands/directory/ListInstalls.js +81 -0
  51. package/dist/commands/directory/ListInstalls.js.map +1 -0
  52. package/dist/commands/directory/Publish.d.ts +8 -0
  53. package/dist/commands/directory/Publish.js +180 -0
  54. package/dist/commands/directory/Publish.js.map +1 -0
  55. package/dist/commands/directory/Status.d.ts +5 -0
  56. package/dist/commands/directory/Status.js +60 -0
  57. package/dist/commands/directory/Status.js.map +1 -0
  58. package/dist/commands/directory/Uninstall.d.ts +6 -0
  59. package/dist/commands/directory/Uninstall.js +50 -0
  60. package/dist/commands/directory/Uninstall.js.map +1 -0
  61. package/dist/commands/directory/Unpublish.d.ts +10 -0
  62. package/dist/commands/directory/Unpublish.js +181 -0
  63. package/dist/commands/directory/Unpublish.js.map +1 -0
  64. package/dist/commands/directory/Uprade.d.ts +8 -0
  65. package/dist/commands/directory/Uprade.js +100 -0
  66. package/dist/commands/directory/Uprade.js.map +1 -0
  67. package/dist/commands/env/GetEnvironment.d.ts +3 -0
  68. package/dist/commands/env/GetEnvironment.js +28 -0
  69. package/dist/commands/env/GetEnvironment.js.map +1 -0
  70. package/dist/commands/env/SetEnvironment.d.ts +4 -0
  71. package/dist/commands/env/SetEnvironment.js +63 -0
  72. package/dist/commands/env/SetEnvironment.js.map +1 -0
  73. package/dist/commands/jobs/List.d.ts +21 -0
  74. package/dist/commands/jobs/List.js +268 -0
  75. package/dist/commands/jobs/List.js.map +1 -0
  76. package/dist/commands/jobs/RuntimeStatus.d.ts +5 -0
  77. package/dist/commands/jobs/RuntimeStatus.js +65 -0
  78. package/dist/commands/jobs/RuntimeStatus.js.map +1 -0
  79. package/dist/commands/jobs/Terminate.d.ts +5 -0
  80. package/dist/commands/jobs/Terminate.js +45 -0
  81. package/dist/commands/jobs/Terminate.js.map +1 -0
  82. package/dist/commands/jobs/Trigger.d.ts +8 -0
  83. package/dist/commands/jobs/Trigger.js +58 -0
  84. package/dist/commands/jobs/Trigger.js.map +1 -0
  85. package/dist/commands/review/List.d.ts +5 -0
  86. package/dist/commands/review/List.js +81 -0
  87. package/dist/commands/review/List.js.map +1 -0
  88. package/dist/commands/review/Open.d.ts +4 -0
  89. package/dist/commands/review/Open.js +42 -0
  90. package/dist/commands/review/Open.js.map +1 -0
  91. package/dist/index.d.ts +1 -0
  92. package/dist/index.js +15 -0
  93. package/dist/index.js.map +1 -0
  94. package/dist/lib/AppContext.d.ts +9 -0
  95. package/dist/lib/AppContext.js +43 -0
  96. package/dist/lib/AppContext.js.map +1 -0
  97. package/dist/lib/AppPackager.d.ts +8 -0
  98. package/dist/lib/AppPackager.js +40 -0
  99. package/dist/lib/AppPackager.js.map +1 -0
  100. package/dist/lib/AppUpdater.d.ts +9 -0
  101. package/dist/lib/AppUpdater.js +155 -0
  102. package/dist/lib/AppUpdater.js.map +1 -0
  103. package/dist/lib/AppUploader.d.ts +8 -0
  104. package/dist/lib/AppUploader.js +36 -0
  105. package/dist/lib/AppUploader.js.map +1 -0
  106. package/dist/lib/Config.d.ts +15 -0
  107. package/dist/lib/Config.js +47 -0
  108. package/dist/lib/Config.js.map +1 -0
  109. package/dist/lib/EnvironmentalOutput.d.ts +1 -0
  110. package/dist/lib/EnvironmentalOutput.js +21 -0
  111. package/dist/lib/EnvironmentalOutput.js.map +1 -0
  112. package/dist/lib/Moria.d.ts +51 -0
  113. package/dist/lib/Moria.js +30 -0
  114. package/dist/lib/Moria.js.map +1 -0
  115. package/dist/lib/MoriaApi.d.ts +22 -0
  116. package/dist/lib/MoriaApi.js +75 -0
  117. package/dist/lib/MoriaApi.js.map +1 -0
  118. package/dist/lib/Rivendell.d.ts +351 -0
  119. package/dist/lib/Rivendell.js +328 -0
  120. package/dist/lib/Rivendell.js.map +1 -0
  121. package/dist/lib/RivendellApi.d.ts +22 -0
  122. package/dist/lib/RivendellApi.js +90 -0
  123. package/dist/lib/RivendellApi.js.map +1 -0
  124. package/dist/lib/Shards.d.ts +3 -0
  125. package/dist/lib/Shards.js +34 -0
  126. package/dist/lib/Shards.js.map +1 -0
  127. package/dist/lib/StringUtils.d.ts +1 -0
  128. package/dist/lib/StringUtils.js +9 -0
  129. package/dist/lib/StringUtils.js.map +1 -0
  130. package/dist/lib/TeminalPassthru.d.ts +5 -0
  131. package/dist/lib/TeminalPassthru.js +12 -0
  132. package/dist/lib/TeminalPassthru.js.map +1 -0
  133. package/dist/lib/TerminalConfirm.d.ts +3 -0
  134. package/dist/lib/TerminalConfirm.js +26 -0
  135. package/dist/lib/TerminalConfirm.js.map +1 -0
  136. package/dist/lib/TerminalInput.d.ts +32 -0
  137. package/dist/lib/TerminalInput.js +207 -0
  138. package/dist/lib/TerminalInput.js.map +1 -0
  139. package/dist/lib/TerminalMenu.d.ts +34 -0
  140. package/dist/lib/TerminalMenu.js +186 -0
  141. package/dist/lib/TerminalMenu.js.map +1 -0
  142. package/dist/lib/TerminalOutput.d.ts +5 -0
  143. package/dist/lib/TerminalOutput.js +12 -0
  144. package/dist/lib/TerminalOutput.js.map +1 -0
  145. package/dist/lib/TerminalSpinner.d.ts +15 -0
  146. package/dist/lib/TerminalSpinner.js +71 -0
  147. package/dist/lib/TerminalSpinner.js.map +1 -0
  148. package/dist/lib/build.d.ts +5 -0
  149. package/dist/lib/build.js +35 -0
  150. package/dist/lib/build.js.map +1 -0
  151. package/dist/lib/checkForUpdate.d.ts +2 -0
  152. package/dist/lib/checkForUpdate.js +58 -0
  153. package/dist/lib/checkForUpdate.js.map +1 -0
  154. package/dist/lib/dev/app.d.ts +8 -0
  155. package/dist/lib/dev/app.js +53 -0
  156. package/dist/lib/dev/app.js.map +1 -0
  157. package/dist/lib/dev/index.d.ts +1 -0
  158. package/dist/lib/dev/index.js +14 -0
  159. package/dist/lib/dev/index.js.map +1 -0
  160. package/dist/lib/dev/logger.d.ts +15 -0
  161. package/dist/lib/dev/logger.js +58 -0
  162. package/dist/lib/dev/logger.js.map +1 -0
  163. package/dist/lib/die.d.ts +5 -0
  164. package/dist/lib/die.js +14 -0
  165. package/dist/lib/die.js.map +1 -0
  166. package/dist/lib/directoryExists.d.ts +1 -0
  167. package/dist/lib/directoryExists.js +9 -0
  168. package/dist/lib/directoryExists.js.map +1 -0
  169. package/dist/lib/formatBuildState.d.ts +2 -0
  170. package/dist/lib/formatBuildState.js +23 -0
  171. package/dist/lib/formatBuildState.js.map +1 -0
  172. package/dist/lib/formatError.d.ts +1 -0
  173. package/dist/lib/formatError.js +45 -0
  174. package/dist/lib/formatError.js.map +1 -0
  175. package/dist/lib/formatJobStatus.d.ts +3 -0
  176. package/dist/lib/formatJobStatus.js +28 -0
  177. package/dist/lib/formatJobStatus.js.map +1 -0
  178. package/dist/lib/formatReviewStatus.d.ts +4 -0
  179. package/dist/lib/formatReviewStatus.js +28 -0
  180. package/dist/lib/formatReviewStatus.js.map +1 -0
  181. package/dist/lib/formatTimstamp.d.ts +1 -0
  182. package/dist/lib/formatTimstamp.js +25 -0
  183. package/dist/lib/formatTimstamp.js.map +1 -0
  184. package/dist/lib/formatVersionState.d.ts +2 -0
  185. package/dist/lib/formatVersionState.js +35 -0
  186. package/dist/lib/formatVersionState.js.map +1 -0
  187. package/dist/lib/gatherAppEnv.d.ts +3 -0
  188. package/dist/lib/gatherAppEnv.js +71 -0
  189. package/dist/lib/gatherAppEnv.js.map +1 -0
  190. package/dist/lib/handleInterrupt.d.ts +1 -0
  191. package/dist/lib/handleInterrupt.js +17 -0
  192. package/dist/lib/handleInterrupt.js.map +1 -0
  193. package/dist/lib/jobRuntime.d.ts +3 -0
  194. package/dist/lib/jobRuntime.js +16 -0
  195. package/dist/lib/jobRuntime.js.map +1 -0
  196. package/dist/lib/parseDate.d.ts +2 -0
  197. package/dist/lib/parseDate.js +26 -0
  198. package/dist/lib/parseDate.js.map +1 -0
  199. package/dist/lib/templating/TemplateRenderer.d.ts +13 -0
  200. package/dist/lib/templating/TemplateRenderer.js +62 -0
  201. package/dist/lib/templating/TemplateRenderer.js.map +1 -0
  202. package/dist/lib/templating/fetchTemplatesManifest.d.ts +1 -0
  203. package/dist/lib/templating/fetchTemplatesManifest.js +10 -0
  204. package/dist/lib/templating/fetchTemplatesManifest.js.map +1 -0
  205. package/dist/lib/templating/types.d.ts +27 -0
  206. package/dist/lib/templating/types.js +3 -0
  207. package/dist/lib/templating/types.js.map +1 -0
  208. package/dist/oo-cli.manifest.json +1142 -0
  209. package/dist/test/setup.d.ts +0 -0
  210. package/dist/test/setup.js +4 -0
  211. package/dist/test/setup.js.map +1 -0
  212. package/package.json +94 -0
  213. package/src/commands/accounts/Whoami.ts +19 -0
  214. package/src/commands/accounts/Whois.ts +51 -0
  215. package/src/commands/app/BaseBuildCommand.ts +266 -0
  216. package/src/commands/app/Init.ts +303 -0
  217. package/src/commands/app/Logs.ts +241 -0
  218. package/src/commands/app/Package.ts +39 -0
  219. package/src/commands/app/Prepare.ts +108 -0
  220. package/src/commands/app/Register.ts +41 -0
  221. package/src/commands/app/Validate.ts +13 -0
  222. package/src/commands/availability/List.ts +37 -0
  223. package/src/commands/directory/Info.ts +83 -0
  224. package/src/commands/directory/Install.ts +37 -0
  225. package/src/commands/directory/List.ts +96 -0
  226. package/src/commands/directory/ListFunctions.ts +60 -0
  227. package/src/commands/directory/ListGlobalFunctions.ts +54 -0
  228. package/src/commands/directory/ListInstalls.ts +73 -0
  229. package/src/commands/directory/Publish.ts +179 -0
  230. package/src/commands/directory/Status.ts +45 -0
  231. package/src/commands/directory/Uninstall.ts +32 -0
  232. package/src/commands/directory/Unpublish.ts +173 -0
  233. package/src/commands/directory/Uprade.ts +85 -0
  234. package/src/commands/env/GetEnvironment.ts +14 -0
  235. package/src/commands/env/SetEnvironment.ts +52 -0
  236. package/src/commands/jobs/List.ts +278 -0
  237. package/src/commands/jobs/RuntimeStatus.ts +49 -0
  238. package/src/commands/jobs/Terminate.ts +29 -0
  239. package/src/commands/jobs/Trigger.ts +41 -0
  240. package/src/commands/review/List.ts +76 -0
  241. package/src/commands/review/Open.ts +28 -0
  242. package/src/index.ts +15 -0
  243. package/src/lib/AppContext.ts +47 -0
  244. package/src/lib/AppPackager.ts +47 -0
  245. package/src/lib/AppUpdater.ts +177 -0
  246. package/src/lib/AppUploader.ts +39 -0
  247. package/src/lib/Config.ts +60 -0
  248. package/src/lib/EnvironmentalOutput.ts +18 -0
  249. package/src/lib/Moria.ts +66 -0
  250. package/src/lib/MoriaApi.ts +86 -0
  251. package/src/lib/Rivendell.ts +572 -0
  252. package/src/lib/RivendellApi.ts +99 -0
  253. package/src/lib/Shards.ts +37 -0
  254. package/src/lib/StringUtils.ts +4 -0
  255. package/src/lib/TeminalPassthru.ts +7 -0
  256. package/src/lib/TerminalConfirm.ts +27 -0
  257. package/src/lib/TerminalInput.ts +236 -0
  258. package/src/lib/TerminalMenu.ts +221 -0
  259. package/src/lib/TerminalOutput.ts +7 -0
  260. package/src/lib/TerminalSpinner.ts +76 -0
  261. package/src/lib/build.ts +36 -0
  262. package/src/lib/checkForUpdate.ts +63 -0
  263. package/src/lib/dev/app.ts +58 -0
  264. package/src/lib/dev/index.ts +1 -0
  265. package/src/lib/dev/logger.ts +77 -0
  266. package/src/lib/die.ts +10 -0
  267. package/src/lib/directoryExists.ts +5 -0
  268. package/src/lib/formatBuildState.ts +20 -0
  269. package/src/lib/formatError.ts +39 -0
  270. package/src/lib/formatJobStatus.ts +24 -0
  271. package/src/lib/formatReviewStatus.ts +27 -0
  272. package/src/lib/formatTimstamp.ts +21 -0
  273. package/src/lib/formatVersionState.ts +31 -0
  274. package/src/lib/gatherAppEnv.ts +75 -0
  275. package/src/lib/handleInterrupt.ts +13 -0
  276. package/src/lib/jobRuntime.ts +12 -0
  277. package/src/lib/parseDate.ts +21 -0
  278. package/src/lib/templating/TemplateRenderer.ts +65 -0
  279. package/src/lib/templating/fetchTemplatesManifest.ts +6 -0
  280. package/src/lib/templating/types.ts +30 -0
  281. package/src/test/setup.ts +2 -0
  282. package/src/types/columnify.d.ts +27 -0
  283. package/src/types/gitignore-parser.d.ts +11 -0
@@ -0,0 +1,99 @@
1
+ import fetch, {RequestInit, Response} from 'node-fetch';
2
+ import * as path from 'path';
3
+ import {getActiveCredential, runtimeConfig} from './Config';
4
+ import {die} from './die';
5
+
6
+ export type Method = 'POST' | 'PUT' | 'PATCH' | 'GET' | 'DELETE';
7
+
8
+ /**
9
+ * Low level http handler for interacting with the Rivendell api.
10
+ */
11
+ export namespace RivendellApi {
12
+ interface ApiResponse<T = any> {
13
+ response: Response;
14
+ body: T;
15
+ }
16
+
17
+ export class ApiError extends Error {
18
+ constructor(message: string, public responseText: string, public response?: Response) {
19
+ super(message);
20
+ }
21
+ }
22
+
23
+ export async function get<T>(uri: string): Promise<ApiResponse<T>> {
24
+ return request('GET', uri);
25
+ }
26
+
27
+ export async function post<T>(uri: string, body: any): Promise<ApiResponse<T>> {
28
+ return request('POST', uri, body);
29
+ }
30
+
31
+ export async function put<T>(uri: string, body: any): Promise<ApiResponse<T>> {
32
+ return request('PUT', uri, body);
33
+ }
34
+
35
+ export async function delete_<T>(uri: string): Promise<ApiResponse<T>> {
36
+ return request('DELETE', uri);
37
+ }
38
+
39
+ export async function request<T>(method: Method, uri: string, body?: any): Promise<ApiResponse<T>> {
40
+ const url = `${runtimeConfig().rivendell}/${uri}`;
41
+ const requestPayload: RequestInit = { method };
42
+
43
+ requestPayload.headers = {
44
+ 'x-api-key': loadApiKey(),
45
+ 'x-cli-version': require(path.join(__dirname, '../oo-cli.manifest.json'))['package']['version']
46
+ };
47
+ if (body && method !== 'GET') {
48
+ // @ts-ignore
49
+ requestPayload.headers['content-type'] = 'application/json';
50
+ requestPayload.body = JSON.stringify(body);
51
+ }
52
+
53
+ const response = await fetch(url, requestPayload);
54
+ const responseText = await response.text();
55
+
56
+ if (response.status < 200 || response.status >= 300) {
57
+ const errorMsg = `Received a ${response.status} from OCP: ${getErrorMessage(response.status)}`;
58
+ throw new ApiError(errorMsg, responseText, response);
59
+ }
60
+
61
+ return {
62
+ response,
63
+ body: responseText ? JSON.parse(responseText) : ''
64
+ };
65
+ }
66
+
67
+ function loadApiKey(): string {
68
+ const apiKey = getActiveCredential().apiKey;
69
+ if (apiKey == null) {
70
+ die(
71
+ 'Your API key is not configured in ~/.ocp/credentials.json. ' +
72
+ 'Please assign your API key to the property apiKey in the config file.'
73
+ );
74
+ }
75
+
76
+ return apiKey!;
77
+ }
78
+
79
+ function getErrorMessage(code: number): string {
80
+ switch (code) {
81
+ case 400:
82
+ return 'Bad request.';
83
+ case 403:
84
+ return 'Access denied.';
85
+ case 404:
86
+ return 'Not found.';
87
+ case 500:
88
+ return 'Internal service error.';
89
+ case 502:
90
+ return 'Bad gateway';
91
+ case 503:
92
+ return 'Service unavailable. Please try again.';
93
+ case 504:
94
+ return 'Request timed out. Please try again.';
95
+ default:
96
+ return 'Unhandled error.';
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,37 @@
1
+ import { AppManifest } from '@zaiusinc/app-sdk';
2
+ import { prerelease } from 'semver';
3
+ import { readAppYaml } from './AppContext';
4
+ import { getEnv } from './Config';
5
+ import { Rivendell } from './Rivendell';
6
+
7
+ export async function applicableShards(availability = '', fromManifest?: AppManifest): Promise<string[]> {
8
+ if (getEnv() === 'staging') {
9
+ return ['us'];
10
+ }
11
+
12
+ const manifest = fromManifest || readAppYaml();
13
+ const pre = prerelease(manifest.meta.version);
14
+
15
+ if (pre && pre[0] === 'dev') {
16
+ return ['us'];
17
+ }
18
+
19
+ if (availability) {
20
+ return [availability];
21
+ }
22
+
23
+ return resolveShards(manifest);
24
+ }
25
+
26
+ export async function targetShards(): Promise<string[]> {
27
+ return await resolveShards(readAppYaml());
28
+ }
29
+
30
+ async function resolveShards(manifest: AppManifest) {
31
+ let shards = manifest.meta.availability || ['us'];
32
+ if (shards.includes('all')) {
33
+ shards = (await Rivendell.shards()).map((s) => s.id);
34
+ }
35
+
36
+ return ['us'].concat(shards.filter((s) => s !== 'us').sort());
37
+ }
@@ -0,0 +1,4 @@
1
+ // Copied from https://github.com/zeit/now-cli/blob/45097563f09b412ccbbb5b07838b587ea669a531/src/util/strlen.ts
2
+ export function styledStringLength(str: string): number {
3
+ return str.replace(/\u001b[^m]*m/g, '').length;
4
+ }
@@ -0,0 +1,7 @@
1
+ import * as child_process from 'child_process';
2
+
3
+ export namespace TerminalPassthru {
4
+ export function exec(command: string) {
5
+ return child_process.spawnSync(command, {cwd: process.cwd(), shell: true, stdio: 'inherit'});
6
+ }
7
+ }
@@ -0,0 +1,27 @@
1
+ import {TerminalInput} from './TerminalInput';
2
+
3
+ const yes = ['y', 'yes'];
4
+ const no = ['n', 'no'];
5
+
6
+ const validate = (value: string) => (
7
+ yes.includes(value.toLowerCase()) || no.includes(value.toLowerCase())
8
+ );
9
+
10
+ const getHint = (value: string) => {
11
+ if (value && !validate(value)) {
12
+ return 'Enter yes or no (y/n)';
13
+ }
14
+ return '';
15
+ };
16
+
17
+ export namespace TerminalConfirm {
18
+ export async function ask(question: string, hint: string = '') {
19
+ const result = await TerminalInput.ask(question, {
20
+ getHint: (value) => getHint(value) || hint,
21
+ validate,
22
+ clearOnInvalidInput: true
23
+ });
24
+
25
+ return yes.includes(result.toLowerCase());
26
+ }
27
+ }
@@ -0,0 +1,236 @@
1
+ import chalk from 'chalk';
2
+ import {stringWidth, terminal} from 'terminal-kit';
3
+
4
+ interface Coordinates {
5
+ x: number;
6
+ y: number;
7
+ }
8
+
9
+ export interface InputOptions {
10
+ getHint?: (value: string) => string;
11
+ validate?: (value: string) => boolean;
12
+ clearAfter?: boolean;
13
+ defaultValue?: string;
14
+ clearOnInvalidInput?: boolean;
15
+ }
16
+
17
+ export class TerminalInput {
18
+ public static ask(question: string, options?: InputOptions): Promise<string> {
19
+ return new Promise(async (resolve, _reject) => {
20
+ // tslint:disable-next-line:no-unused-expression
21
+ await new TerminalInput(question, options || {}, (value) => resolve(value)).start();
22
+ });
23
+ }
24
+
25
+ private input = '';
26
+ private y = 0;
27
+ private inputCoords!: Coordinates;
28
+ private cursorOffset = 0;
29
+ private hint = '';
30
+ private hintY = 0;
31
+ private isValid = true;
32
+
33
+ constructor(
34
+ private question: string,
35
+ private options: InputOptions,
36
+ private onEnter: (value: string) => void,
37
+ ) {
38
+ this.hint = this.getHint();
39
+ if (this.options.validate) {
40
+ this.isValid = this.options.validate(options.defaultValue || '');
41
+ }
42
+ }
43
+
44
+ private async start() {
45
+ terminal.grabInput(true);
46
+ terminal.on('key', this.onKeyPress);
47
+ terminal.on('resize', this.onResize);
48
+ this.y = (await (terminal as any).getCursorLocation()).y;
49
+ await this.render();
50
+ this.placeCursor();
51
+ }
52
+
53
+ private async end() {
54
+ terminal.grabInput(false);
55
+ terminal.off('key', this.onKeyPress);
56
+ terminal.off('resize', this.onResize);
57
+ if (this.options.clearAfter === false) {
58
+ // use render to place cursor at end
59
+ await this.render();
60
+ terminal('\n').eraseDisplayBelow();
61
+ } else {
62
+ terminal.moveTo(1, this.y).eraseDisplayBelow();
63
+ }
64
+ }
65
+
66
+ private get defaultHint() {
67
+ if (!this.input && this.options.defaultValue) {
68
+ return `[Enter] to accept ${this.options.defaultValue}`;
69
+ }
70
+ return '';
71
+ }
72
+
73
+ private getHint() {
74
+ if (this.options.getHint) {
75
+ return this.options.getHint(this.input || this.options.defaultValue || '') || this.defaultHint;
76
+ }
77
+ return this.defaultHint;
78
+ }
79
+
80
+ private async updateInput(input: string, resetHint: boolean = true) {
81
+ if (this.input !== input) {
82
+ this.input = input;
83
+ let needsRerender = false;
84
+ if (resetHint) {
85
+ const hint = this.getHint();
86
+ if (hint !== this.hint) {
87
+ this.hint = hint;
88
+ needsRerender = true;
89
+ }
90
+ }
91
+ if (this.options.validate) {
92
+ const valid = this.options.validate(input || this.options.defaultValue || '');
93
+ if (valid !== this.isValid) {
94
+ needsRerender = true;
95
+ this.isValid = valid;
96
+ }
97
+ }
98
+
99
+ if (needsRerender) {
100
+ await this.render();
101
+ } else {
102
+ await this.renderPartial(this.cursorOffset);
103
+ }
104
+ }
105
+ }
106
+
107
+ private spliceInput(index: number, del: number, insert: string = '') {
108
+ return this.input.slice(0, index) + insert + this.input.slice(index + del);
109
+ }
110
+
111
+ private onKeyPress = async (key: string, _matches: string[], data: {isCharacter: boolean}) => {
112
+ if (data.isCharacter) {
113
+ await this.updateInput(this.spliceInput(this.cursorOffset, 0, key));
114
+ this.cursorOffset += stringWidth(key);
115
+ this.placeCursor();
116
+ } else {
117
+ switch (key) {
118
+ case 'LEFT':
119
+ this.cursorOffset = Math.max(0, this.cursorOffset - 1);
120
+ this.placeCursor();
121
+ break;
122
+
123
+ case 'RIGHT':
124
+ this.cursorOffset = Math.min(stringWidth(this.input), this.cursorOffset + 1);
125
+ this.placeCursor();
126
+ break;
127
+
128
+ case 'HOME':
129
+ this.cursorOffset = 0;
130
+ this.placeCursor();
131
+ break;
132
+
133
+ case 'END':
134
+ this.cursorOffset = stringWidth(this.input);
135
+ this.placeCursor();
136
+ break;
137
+
138
+ case 'DELETE':
139
+ await this.updateInput(this.spliceInput(this.cursorOffset, 1));
140
+ this.placeCursor();
141
+ break;
142
+
143
+ case 'BACKSPACE':
144
+ if (this.cursorOffset > 0) {
145
+ this.cursorOffset--;
146
+ await this.updateInput(this.spliceInput(this.cursorOffset, 1));
147
+ this.placeCursor();
148
+ }
149
+ break;
150
+
151
+ case 'ENTER':
152
+ case 'KP_ENTER':
153
+ if (this.isValid) {
154
+ await this.end();
155
+ this.onEnter(this.input || this.options.defaultValue || '');
156
+ } else if (this.options.clearOnInvalidInput) {
157
+ this.cursorOffset = 0;
158
+ await this.updateInput('', false);
159
+ this.placeCursor();
160
+ }
161
+ break;
162
+
163
+ case 'CTRL_D':
164
+ case 'CTRL_C':
165
+ terminal('\Bye...');
166
+ terminal.processExit(1);
167
+ }
168
+ }
169
+ }
170
+
171
+ private onResize = async () => {
172
+ terminal.moveTo(1, this.y).eraseDisplayBelow();
173
+ await this.render();
174
+ this.placeCursor();
175
+ }
176
+
177
+ private async render() {
178
+ const hint = ' ' + this.hint;
179
+ const question = this.question + ' ';
180
+
181
+ // compute where we should be after rendering
182
+ this.inputCoords = {
183
+ x: 1 + stringWidth(question) % terminal.width,
184
+ y: this.y + Math.floor((stringWidth(question) - 1) / terminal.width)
185
+ };
186
+ this.hintY = this.y
187
+ + Math.floor((stringWidth(question) + stringWidth(this.input) - 1) / terminal.width)
188
+ + 1;
189
+ let expectedY = this.hintY + Math.floor((stringWidth(hint) - 1) / terminal.width);
190
+ const inputEndX = (this.inputCoords.x - 1 + stringWidth(this.input)) % terminal.width;
191
+
192
+ // offset our start position to account for scrolling due to long input or hint
193
+ while (expectedY > terminal.height) {
194
+ terminal.moveTo(terminal.width, terminal.height);
195
+ terminal('\n');
196
+ expectedY--;
197
+ this.hintY--;
198
+ this.inputCoords.y--;
199
+ this.y--;
200
+ }
201
+
202
+ // render input and hint
203
+ terminal.moveTo(terminal.width, this.y - 1).defaultColor('\n');
204
+ terminal(question + (this.isValid ? this.input : chalk.red(this.input)));
205
+ if (inputEndX !== 0) {
206
+ terminal.eraseLineAfter();
207
+ }
208
+ terminal('\n').gray(hint).eraseDisplayBelow();
209
+ }
210
+
211
+ private async renderPartial(offset: number) {
212
+ const endY = this.inputCoords.y
213
+ + Math.floor((stringWidth(this.input) + this.inputCoords.x - 1) / terminal.width)
214
+ + 1;
215
+ const endX = (this.inputCoords.x - 1 + stringWidth(this.input)) % terminal.width;
216
+ if (endY >= this.hintY) {
217
+ await this.render();
218
+ } else {
219
+ const content = this.input.slice(offset);
220
+ terminal((this.isValid ? content : chalk.red(content)));
221
+ if (endX !== 0) {
222
+ terminal.eraseLineAfter();
223
+ }
224
+ }
225
+ }
226
+
227
+ private placeCursor() {
228
+ if (this.inputCoords.x + this.cursorOffset < terminal.width) {
229
+ terminal.moveTo(this.inputCoords.x + this.cursorOffset, this.inputCoords.y);
230
+ } else {
231
+ const x = 1 + (this.inputCoords.x - 1 + this.cursorOffset) % terminal.width;
232
+ const y = this.inputCoords.y + Math.floor((this.inputCoords.x - 1 + this.cursorOffset) / terminal.width);
233
+ terminal.moveTo(x, y);
234
+ }
235
+ }
236
+ }
@@ -0,0 +1,221 @@
1
+ import chalk from 'chalk';
2
+ import {stringWidth, terminal, truncateString} from 'terminal-kit';
3
+
4
+ export interface MenuOptions {
5
+ layout?: 'grid' | 'row';
6
+ clearAfter?: boolean;
7
+ }
8
+
9
+ export interface MenuChoice {
10
+ text: string;
11
+ id?: string | number;
12
+ }
13
+
14
+ interface Layout {
15
+ x: number;
16
+ y: number;
17
+ index: number;
18
+ text: string;
19
+ }
20
+
21
+ export class TerminalMenu {
22
+ public static ask(question: string, choices: MenuChoice[] | string[], options?: MenuOptions): Promise<MenuChoice> {
23
+ return new Promise(async (resolve, _reject) => {
24
+ await new TerminalMenu(question, choices, options || {}, (sel) => resolve(sel)).start();
25
+ });
26
+ }
27
+
28
+ private choices: MenuChoice[];
29
+ private y!: number;
30
+ private layout!: Layout[][]; // represented as a grid[x][y]
31
+ private questionHeight: number = 1;
32
+ private selected: number = 0;
33
+ private scrollOffset: number = 0;
34
+
35
+ constructor(
36
+ private question: string,
37
+ choices: MenuChoice[] | string[],
38
+ private options: MenuOptions,
39
+ private onSelect: (selection: MenuChoice) => void
40
+ ) {
41
+ if (choices.length > 0 && typeof choices[0] === 'string') {
42
+ this.choices = (choices as string[]).map((text, id) => ({text, id}));
43
+ } else {
44
+ this.choices = choices as MenuChoice[];
45
+ }
46
+ }
47
+
48
+ private async start() {
49
+ terminal.grabInput(true);
50
+ terminal.on('key', this.onKeyPress);
51
+ terminal.on('resize', this.onResize);
52
+ this.y = (await (terminal as any).getCursorLocation()).y;
53
+ await this.layoutChoices();
54
+ this.render();
55
+ }
56
+
57
+ private end() {
58
+ terminal.grabInput(false);
59
+ terminal.off('key', this.onKeyPress);
60
+ terminal.off('resize', this.onResize);
61
+ if (this.options.clearAfter === false) {
62
+ const {y} = this.getCoordinates(this.choices.length - 1);
63
+ terminal.moveTo(1, y).nextLine(1).eraseDisplayBelow();
64
+ } else {
65
+ terminal.moveTo(1, this.y).eraseDisplayBelow();
66
+ }
67
+ }
68
+
69
+ private get menuTop() {
70
+ return this.y + this.questionHeight;
71
+ }
72
+
73
+ private scrollSelectionIntoView() {
74
+ const previous = this.scrollOffset;
75
+ let {y} = this.getCoordinates(this.selected);
76
+ while (y < this.menuTop) {
77
+ y++;
78
+ this.scrollOffset--;
79
+ }
80
+ while (y > terminal.height) {
81
+ y--;
82
+ this.scrollOffset++;
83
+ }
84
+ if (previous !== this.scrollOffset) {
85
+ this.render();
86
+ }
87
+ }
88
+
89
+ private setSelection(index: number) {
90
+ const previous = this.selected;
91
+ this.selected = index;
92
+ this.scrollSelectionIntoView();
93
+ this.rerender(previous);
94
+ this.rerender(this.selected);
95
+ this.cursorToSelected();
96
+ }
97
+
98
+ private onResize = async () => {
99
+ await this.layoutChoices();
100
+ this.render();
101
+ }
102
+
103
+ private onKeyPress = (name: string, _matches: string[], _data: {code: string}) => {
104
+ const {x, y} = this.getOffset(this.selected);
105
+ switch (name) {
106
+ case 'UP':
107
+ if (y > 0) {
108
+ this.setSelection(this.layout[x][y - 1].index);
109
+ }
110
+ break;
111
+
112
+ case 'DOWN':
113
+ if (y < this.layout[x].length - 1) {
114
+ this.setSelection(this.layout[x][y + 1].index);
115
+ }
116
+ break;
117
+
118
+ case 'LEFT':
119
+ if (x > 0) {
120
+ this.setSelection(this.layout[x - 1][y].index);
121
+ }
122
+ break;
123
+
124
+ case 'RIGHT':
125
+ if (this.layout[x + 1] && this.layout[x + 1][y]) {
126
+ this.setSelection(this.layout[x + 1][y].index);
127
+ }
128
+ break;
129
+
130
+ case 'ENTER':
131
+ case 'KP_ENTER':
132
+ this.end();
133
+ this.onSelect(this.choices[this.selected]);
134
+ break;
135
+ }
136
+ }
137
+
138
+ private getOffset(index: number) {
139
+ for (let x = 0; x < this.layout.length; x++) {
140
+ for (let y = 0; y < this.layout[x].length; y++) {
141
+ if (this.layout[x][y].index === index) {
142
+ return {x, y};
143
+ }
144
+ }
145
+ }
146
+ return {x: 0, y: 0};
147
+ }
148
+
149
+ private getCoordinates(index: number) {
150
+ const {x, y} = this.getOffset(index);
151
+ const layout = this.layout[x][y];
152
+ return {
153
+ x: layout.x,
154
+ y: this.menuTop + layout.y - this.scrollOffset
155
+ };
156
+
157
+ }
158
+
159
+ private render() {
160
+ terminal.moveTo(1, this.y).eraseDisplayBelow();
161
+ this.questionHeight = Math.floor(stringWidth(this.question) / terminal.width) + 1;
162
+ terminal.moveTo(0, terminal.height);
163
+ while (this.y > 1 && this.menuTop + this.layout[0].length > terminal.height) {
164
+ this.y--;
165
+ console.log('');
166
+ }
167
+
168
+ terminal.moveTo(1, this.y).eraseDisplayBelow();
169
+ terminal(this.question);
170
+ for (const layouts of this.layout) {
171
+ for (const layout of layouts) {
172
+ const y = this.menuTop + layout.y - this.scrollOffset;
173
+ if (y >= this.menuTop && y <= terminal.height) {
174
+ terminal.moveTo(layout.x, y);
175
+ if (layout.index === this.selected) {
176
+ terminal(chalk.bgWhite.black(` ${layout.text} `));
177
+ } else {
178
+ terminal(` ${layout.text} `);
179
+ }
180
+ }
181
+ }
182
+ }
183
+ this.cursorToSelected();
184
+ }
185
+
186
+ private rerender(index: number) {
187
+ const {x, y} = this.getOffset(index);
188
+ const layout = this.layout[x][y];
189
+ terminal.moveTo(layout.x, this.menuTop + layout.y - this.scrollOffset);
190
+ if (layout.index === this.selected) {
191
+ terminal(chalk.bgWhite.black(` ${layout.text} `));
192
+ } else {
193
+ terminal(` ${layout.text} `);
194
+ }
195
+ }
196
+
197
+ private cursorToSelected() {
198
+ const {x, y} = this.getCoordinates(this.selected);
199
+ terminal.moveTo(x, y);
200
+ }
201
+
202
+ private async layoutChoices() {
203
+ const width = terminal.width;
204
+ const maxLength = this.choices.reduce((max, c) => Math.max(max, stringWidth(c.text)), 0);
205
+ const perRow = this.options.layout === 'row' ? 1 : Math.floor(width / (maxLength + 2));
206
+ this.layout = [];
207
+ this.choices.forEach((choice, index) => {
208
+ const x = index % perRow;
209
+ const y = Math.floor(index / perRow);
210
+ if (!this.layout[x]) {
211
+ this.layout[x] = [];
212
+ }
213
+ this.layout[x][y] = {
214
+ x: 1 + (maxLength + 2) * x,
215
+ y,
216
+ index,
217
+ text: stringWidth(choice.text) + 2 > width ? truncateString(choice.text, width - 3) + '…' : choice.text
218
+ };
219
+ });
220
+ }
221
+ }
@@ -0,0 +1,7 @@
1
+ import * as child_process from 'child_process';
2
+
3
+ export namespace TerminalOutput {
4
+ export function exec(command: string) {
5
+ return child_process.spawnSync(command, {cwd: process.cwd(), shell: true, encoding: 'utf-8'});
6
+ }
7
+ }