neonctl 1.28.0 → 1.29.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 (184) hide show
  1. package/.bump +1 -0
  2. package/.editorconfig +7 -0
  3. package/.eslintrc.cjs +15 -0
  4. package/.github/workflows/commitlint.yml +46 -0
  5. package/.github/workflows/pr.yml +25 -0
  6. package/.github/workflows/release.yml +30 -0
  7. package/.husky/commit-msg +4 -0
  8. package/.husky/pre-commit +4 -0
  9. package/.nvmrc +1 -0
  10. package/.prettierignore +3 -0
  11. package/.prettierrc.json +3 -0
  12. package/.releaserc.json +47 -0
  13. package/LICENSE +202 -0
  14. package/commitlint.config.cjs +7 -0
  15. package/generateOptionsFromSpec.ts +68 -0
  16. package/jest/setup.js +5 -0
  17. package/jest.config.ts +199 -0
  18. package/mocks/bin/psql.cjs +9 -0
  19. package/mocks/main/projects/GET.js +27 -0
  20. package/mocks/main/projects/POST.js +22 -0
  21. package/mocks/main/projects/shared/GET.js +16 -0
  22. package/mocks/main/projects/test/DELETE.json +7 -0
  23. package/mocks/main/projects/test/GET.json +13 -0
  24. package/mocks/main/projects/test/PATCH.js +18 -0
  25. package/mocks/main/projects/test/branches/GET.json +25 -0
  26. package/mocks/main/projects/test/branches/POST.js +83 -0
  27. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/DELETE.json +7 -0
  28. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/GET.json +9 -0
  29. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/PATCH.js +14 -0
  30. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/databases/GET.json +6 -0
  31. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/databases/POST.js +13 -0
  32. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/databases/test_db/DELETE.json +6 -0
  33. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/endpoints/GET.json +26 -0
  34. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/endpoints/POST.json +6 -0
  35. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/GET.json +3 -0
  36. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/POST.js +14 -0
  37. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/test_role/DELETE.json +6 -0
  38. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/test_role/reveal_password/GET.json +3 -0
  39. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/set_as_primary/POST.json +9 -0
  40. package/mocks/main/projects/test/branches/br-numbered-branch-123456/GET.json +10 -0
  41. package/mocks/main/projects/test/branches/br-sunny-branch-123456/DELETE.json +7 -0
  42. package/mocks/main/projects/test/branches/br-sunny-branch-123456/GET.json +10 -0
  43. package/mocks/main/projects/test/branches/br-sunny-branch-123456/PATCH.js +14 -0
  44. package/mocks/main/projects/test/branches/br-sunny-branch-123456/databases/GET.json +6 -0
  45. package/mocks/main/projects/test/branches/br-sunny-branch-123456/databases/POST.js +13 -0
  46. package/mocks/main/projects/test/branches/br-sunny-branch-123456/databases/test_db/DELETE.json +6 -0
  47. package/mocks/main/projects/test/branches/br-sunny-branch-123456/endpoints/GET.json +26 -0
  48. package/mocks/main/projects/test/branches/br-sunny-branch-123456/endpoints/POST.json +6 -0
  49. package/mocks/main/projects/test/branches/br-sunny-branch-123456/restore/POST.js +16 -0
  50. package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/GET.json +3 -0
  51. package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/POST.js +14 -0
  52. package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/test_role/DELETE.json +6 -0
  53. package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/test_role/reveal_password/GET.json +3 -0
  54. package/mocks/main/projects/test/branches/br-sunny-branch-123456/set_as_primary/POST.json +9 -0
  55. package/mocks/main/projects/test/endpoints/GET.json +9 -0
  56. package/mocks/main/projects/test/endpoints/POST.js +32 -0
  57. package/mocks/main/projects/test/endpoints/test_endpoint_id/DELETE.json +7 -0
  58. package/mocks/main/projects/test/endpoints/test_endpoint_id/GET.json +9 -0
  59. package/mocks/main/projects/test/endpoints/test_endpoint_id/PATCH.js +17 -0
  60. package/mocks/main/projects/test/operations/GET.json +22 -0
  61. package/mocks/main/users/me/GET.json +5 -0
  62. package/mocks/restore/projects/test/branches/GET.json +21 -0
  63. package/mocks/restore/projects/test/branches/br-another-branch-123456/GET.json +6 -0
  64. package/mocks/restore/projects/test/branches/br-another-branch-123456/restore/POST.js +13 -0
  65. package/mocks/restore/projects/test/branches/br-any-branch-123456/GET.json +6 -0
  66. package/mocks/restore/projects/test/branches/br-parent-tots-123456/GET.json +7 -0
  67. package/mocks/restore/projects/test/branches/br-parent-tots-123456/restore/POST.js +14 -0
  68. package/mocks/restore/projects/test/branches/br-self-tolsn-123456/GET.json +6 -0
  69. package/mocks/restore/projects/test/branches/br-self-tolsn-123456/restore/POST.js +15 -0
  70. package/mocks/single_project/projects/GET.json +10 -0
  71. package/mocks/single_project/projects/test-project-123456/GET.json +14 -0
  72. package/mocks/single_project/projects/test-project-123456/branches/GET.json +11 -0
  73. package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/databases/GET.json +3 -0
  74. package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/endpoints/GET.json +10 -0
  75. package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/roles/GET.json +3 -0
  76. package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/roles/test_role/reveal_password/GET.json +3 -0
  77. package/package.json +6 -5
  78. package/pkg.js +45 -3
  79. package/rollup.config.js +20 -0
  80. package/snapshots/commands/branches.test.snap +221 -0
  81. package/snapshots/commands/connection_string.test.snap +70 -0
  82. package/snapshots/commands/databases.test.snap +20 -0
  83. package/snapshots/commands/ip_allow.test.snap +55 -0
  84. package/snapshots/commands/operations.test.snap +17 -0
  85. package/snapshots/commands/projects.test.snap +141 -0
  86. package/snapshots/commands/roles.test.snap +19 -0
  87. package/snapshots/commands/set_context.test.snap +30 -0
  88. package/snapshots/writer.test.snap +60 -0
  89. package/snapshotsResolver.cjs +32 -0
  90. package/src/analytics.ts +95 -0
  91. package/src/api.ts +44 -0
  92. package/src/auth.ts +137 -0
  93. package/{cli.js → src/cli.ts} +1 -0
  94. package/src/commands/auth.test.ts +62 -0
  95. package/src/commands/auth.ts +148 -0
  96. package/src/commands/branches.test.ts +354 -0
  97. package/src/commands/branches.ts +451 -0
  98. package/src/commands/connection_string.test.ts +250 -0
  99. package/src/commands/connection_string.ts +210 -0
  100. package/src/commands/databases.test.ts +55 -0
  101. package/src/commands/databases.ts +129 -0
  102. package/src/commands/help.test.ts +13 -0
  103. package/{commands/index.js → src/commands/index.ts} +11 -10
  104. package/src/commands/ip_allow.test.ts +86 -0
  105. package/src/commands/ip_allow.ts +202 -0
  106. package/src/commands/operations.test.ts +13 -0
  107. package/src/commands/operations.ts +41 -0
  108. package/src/commands/projects.test.ts +147 -0
  109. package/src/commands/projects.ts +275 -0
  110. package/src/commands/roles.test.ts +46 -0
  111. package/src/commands/roles.ts +100 -0
  112. package/src/commands/set_context.test.ts +64 -0
  113. package/src/commands/set_context.ts +27 -0
  114. package/src/commands/user.ts +21 -0
  115. package/src/config.ts +22 -0
  116. package/src/context.ts +61 -0
  117. package/src/env.ts +7 -0
  118. package/src/errors.ts +24 -0
  119. package/src/help.ts +185 -0
  120. package/src/index.ts +180 -0
  121. package/src/log.ts +16 -0
  122. package/src/parameters.gen.ts +332 -0
  123. package/src/pkg.ts +9 -0
  124. package/src/test_utils/mock_server.ts +27 -0
  125. package/src/test_utils/oauth_server.ts +10 -0
  126. package/src/test_utils/test_cli_command.ts +117 -0
  127. package/src/types.ts +25 -0
  128. package/src/utils/enrichers.ts +73 -0
  129. package/src/utils/formats.test.ts +41 -0
  130. package/src/utils/formats.ts +11 -0
  131. package/src/utils/middlewares.ts +23 -0
  132. package/src/utils/point_in_time.ts +86 -0
  133. package/src/utils/psql.ts +29 -0
  134. package/src/utils/string.ts +8 -0
  135. package/src/utils/ui.ts +64 -0
  136. package/src/writer.test.ts +98 -0
  137. package/src/writer.ts +131 -0
  138. package/tsconfig.json +17 -0
  139. package/analytics.js +0 -78
  140. package/api.js +0 -35
  141. package/auth.js +0 -101
  142. package/commands/auth.js +0 -102
  143. package/commands/auth.test.js +0 -42
  144. package/commands/branches.js +0 -303
  145. package/commands/branches.test.js +0 -321
  146. package/commands/connection_string.js +0 -137
  147. package/commands/connection_string.test.js +0 -204
  148. package/commands/databases.js +0 -79
  149. package/commands/databases.test.js +0 -51
  150. package/commands/help.test.js +0 -11
  151. package/commands/ip_allow.js +0 -135
  152. package/commands/ip_allow.test.js +0 -78
  153. package/commands/operations.js +0 -28
  154. package/commands/operations.test.js +0 -11
  155. package/commands/projects.js +0 -170
  156. package/commands/projects.test.js +0 -132
  157. package/commands/roles.js +0 -57
  158. package/commands/roles.test.js +0 -42
  159. package/commands/set_context.js +0 -22
  160. package/commands/set_context.test.js +0 -53
  161. package/commands/user.js +0 -15
  162. package/config.js +0 -11
  163. package/context.js +0 -48
  164. package/env.js +0 -6
  165. package/errors.js +0 -16
  166. package/help.js +0 -146
  167. package/index.js +0 -168
  168. package/log.js +0 -15
  169. package/parameters.gen.js +0 -322
  170. package/test_utils/mock_server.js +0 -16
  171. package/test_utils/oauth_server.js +0 -9
  172. package/test_utils/test_cli_command.js +0 -80
  173. package/types.js +0 -1
  174. package/utils/enrichers.js +0 -49
  175. package/utils/formats.js +0 -5
  176. package/utils/formats.test.js +0 -32
  177. package/utils/middlewares.js +0 -20
  178. package/utils/point_in_time.js +0 -44
  179. package/utils/psql.js +0 -24
  180. package/utils/string.js +0 -5
  181. package/utils/ui.js +0 -59
  182. package/writer.js +0 -87
  183. package/writer.test.js +0 -86
  184. /package/{callback.html → src/callback.html} +0 -0
@@ -0,0 +1,46 @@
1
+ import { describe } from '@jest/globals';
2
+
3
+ import { testCliCommand } from '../test_utils/test_cli_command.js';
4
+
5
+ describe('roles', () => {
6
+ testCliCommand({
7
+ name: 'list',
8
+ args: ['roles', 'list', '--project-id', 'test', '--branch', 'test_branch'],
9
+ expected: {
10
+ snapshot: true,
11
+ },
12
+ });
13
+
14
+ testCliCommand({
15
+ name: 'create',
16
+ args: [
17
+ 'roles',
18
+ 'create',
19
+ '--project-id',
20
+ 'test',
21
+ '--branch',
22
+ 'test_branch',
23
+ '--name',
24
+ 'test_role',
25
+ ],
26
+ expected: {
27
+ snapshot: true,
28
+ },
29
+ });
30
+
31
+ testCliCommand({
32
+ name: 'delete',
33
+ args: [
34
+ 'roles',
35
+ 'delete',
36
+ 'test_role',
37
+ '--project-id',
38
+ 'test',
39
+ '--branch',
40
+ 'test_branch',
41
+ ],
42
+ expected: {
43
+ snapshot: true,
44
+ },
45
+ });
46
+ });
@@ -0,0 +1,100 @@
1
+ import yargs from 'yargs';
2
+ import { retryOnLock } from '../api.js';
3
+ import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
4
+
5
+ import { BranchScopeProps } from '../types.js';
6
+ import { writer } from '../writer.js';
7
+
8
+ const ROLES_FIELDS = ['name', 'created_at'] as const;
9
+
10
+ export const command = 'roles';
11
+ export const describe = 'Manage roles';
12
+ export const aliases = ['role'];
13
+ export const builder = (argv: yargs.Argv) =>
14
+ argv
15
+ .usage('$0 roles <sub-command> [options]')
16
+ .options({
17
+ 'project-id': {
18
+ describe: 'Project ID',
19
+ type: 'string',
20
+ },
21
+ branch: {
22
+ describe: 'Branch ID or name',
23
+ type: 'string',
24
+ },
25
+ })
26
+ .middleware(fillSingleProject as any)
27
+ .command(
28
+ 'list',
29
+ 'List roles',
30
+ (yargs) => yargs,
31
+ async (args) => await list(args as any),
32
+ )
33
+ .command(
34
+ 'create',
35
+ 'Create a role',
36
+ (yargs) =>
37
+ yargs.options({
38
+ name: {
39
+ describe: 'Role name',
40
+ type: 'string',
41
+ demandOption: true,
42
+ },
43
+ }),
44
+ async (args) => await create(args as any),
45
+ )
46
+ .command(
47
+ 'delete <role>',
48
+ 'Delete a role',
49
+ (yargs) => yargs,
50
+ async (args) => await deleteRole(args as any),
51
+ );
52
+
53
+ export const handler = (args: yargs.Argv) => {
54
+ return args;
55
+ };
56
+
57
+ export const list = async (props: BranchScopeProps) => {
58
+ const branchId = await branchIdFromProps(props);
59
+ const { data } = await props.apiClient.listProjectBranchRoles(
60
+ props.projectId,
61
+ branchId,
62
+ );
63
+ writer(props).end(data.roles, {
64
+ fields: ROLES_FIELDS,
65
+ });
66
+ };
67
+
68
+ export const create = async (
69
+ props: BranchScopeProps & {
70
+ name: string;
71
+ },
72
+ ) => {
73
+ const branchId = await branchIdFromProps(props);
74
+ const { data } = await retryOnLock(() =>
75
+ props.apiClient.createProjectBranchRole(props.projectId, branchId, {
76
+ role: {
77
+ name: props.name,
78
+ },
79
+ }),
80
+ );
81
+ writer(props).end(data.role, {
82
+ fields: ROLES_FIELDS,
83
+ });
84
+ };
85
+
86
+ export const deleteRole = async (
87
+ props: BranchScopeProps & { role: string },
88
+ ) => {
89
+ const branchId = await branchIdFromProps(props);
90
+ const { data } = await retryOnLock(() =>
91
+ props.apiClient.deleteProjectBranchRole(
92
+ props.projectId,
93
+ branchId,
94
+ props.role,
95
+ ),
96
+ );
97
+ writer(props).end(data.role, {
98
+ fields: ROLES_FIELDS,
99
+ });
100
+ };
@@ -0,0 +1,64 @@
1
+ import { tmpdir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { rmSync, writeFileSync } from 'node:fs';
4
+ import { afterAll, describe } from '@jest/globals';
5
+ import { testCliCommand } from '../test_utils/test_cli_command';
6
+
7
+ const CONTEXT_FILE = join(tmpdir(), `neon_${Date.now()}`);
8
+
9
+ describe('set_context', () => {
10
+ afterAll(() => {
11
+ rmSync(CONTEXT_FILE);
12
+ });
13
+
14
+ describe('should set the context', () => {
15
+ testCliCommand({
16
+ name: 'set-context',
17
+ args: [
18
+ 'set-context',
19
+ '--project-id',
20
+ 'test',
21
+ '--context-file',
22
+ CONTEXT_FILE,
23
+ ],
24
+ });
25
+
26
+ testCliCommand({
27
+ name: 'list branches selecting project from the context',
28
+ args: ['branches', 'list', '--context-file', CONTEXT_FILE],
29
+ expected: {
30
+ snapshot: true,
31
+ },
32
+ });
33
+
34
+ const overrideContextFile = join(
35
+ tmpdir(),
36
+ `neon_override_ctx_${Date.now()}`,
37
+ );
38
+ testCliCommand({
39
+ name: 'get branch id overrides context set branch',
40
+ before: async () => {
41
+ writeFileSync(
42
+ overrideContextFile,
43
+ JSON.stringify({
44
+ projectId: 'test',
45
+ branchId: 'br-cloudy-branch-12345678',
46
+ }),
47
+ );
48
+ },
49
+ after: async () => {
50
+ rmSync(overrideContextFile);
51
+ },
52
+ args: [
53
+ 'branches',
54
+ 'get',
55
+ 'br-sunny-branch-123456',
56
+ '--context-file',
57
+ overrideContextFile,
58
+ ],
59
+ expected: {
60
+ snapshot: true,
61
+ },
62
+ });
63
+ });
64
+ });
@@ -0,0 +1,27 @@
1
+ import yargs from 'yargs';
2
+ import { Context, updateContextFile } from '../context.js';
3
+ import { branchIdFromProps } from '../utils/enrichers.js';
4
+ import { BranchScopeProps } from '../types.js';
5
+
6
+ export const command = 'set-context';
7
+ export const describe = 'Set the current context';
8
+ export const builder = (argv: yargs.Argv) =>
9
+ argv.usage('$0 set-context [options]').options({
10
+ 'project-id': {
11
+ describe: 'Project ID',
12
+ type: 'string',
13
+ },
14
+ branch: {
15
+ describe: 'Branch ID or name',
16
+ type: 'string',
17
+ },
18
+ });
19
+
20
+ export const handler = async (props: BranchScopeProps) => {
21
+ const branchId = await branchIdFromProps(props);
22
+ const context: Context = {
23
+ projectId: props.projectId,
24
+ branchId,
25
+ };
26
+ updateContextFile(props.contextFile, context);
27
+ };
@@ -0,0 +1,21 @@
1
+ import yargs from 'yargs';
2
+
3
+ import { CommonProps } from '../types.js';
4
+ import { writer } from '../writer.js';
5
+
6
+ export const command = 'me';
7
+ export const describe = 'Show current user';
8
+ export const builder = (yargs: yargs.Argv) =>
9
+ yargs.option('context-file', {
10
+ hidden: true,
11
+ });
12
+ export const handler = async (args: CommonProps) => {
13
+ await me(args);
14
+ };
15
+
16
+ const me = async (props: CommonProps) => {
17
+ const { data } = await props.apiClient.getCurrentUserInfo();
18
+ writer(props).end(data, {
19
+ fields: ['login', 'email', 'name', 'projects_limit'],
20
+ });
21
+ };
package/src/config.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { join } from 'node:path';
2
+ import { homedir } from 'node:os';
3
+ import { existsSync, mkdirSync } from 'node:fs';
4
+ import yargs from 'yargs';
5
+
6
+ import { isCi } from './env.js';
7
+
8
+ export const CREDENTIALS_FILE = 'credentials.json';
9
+
10
+ export const defaultDir = join(
11
+ process.env.XDG_CONFIG_HOME || join(homedir(), '.config'),
12
+ 'neonctl',
13
+ );
14
+
15
+ export const ensureConfigDir = async ({
16
+ 'config-dir': configDir,
17
+ 'force-auth': forceAuth,
18
+ }: yargs.Arguments<{ 'config-dir': string }>) => {
19
+ if (!existsSync(configDir) && (!isCi() || forceAuth)) {
20
+ mkdirSync(configDir, { recursive: true });
21
+ }
22
+ };
package/src/context.ts ADDED
@@ -0,0 +1,61 @@
1
+ import { accessSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { normalize, resolve } from 'node:path';
4
+ import yargs from 'yargs';
5
+
6
+ export type Context = {
7
+ projectId?: string;
8
+ branchId?: string;
9
+ };
10
+
11
+ const CONTEXT_FILE = '.neon';
12
+ const CHECK_FILES = [CONTEXT_FILE, 'package.json', '.git'];
13
+
14
+ const wrapWithContextFile = (dir: string) => resolve(dir, CONTEXT_FILE);
15
+
16
+ export const currentContextFile = () => {
17
+ const cwd = process.cwd();
18
+ let currentDir = cwd;
19
+ const root = normalize('/');
20
+ const home = homedir();
21
+ while (currentDir !== root && currentDir !== home) {
22
+ for (const file of CHECK_FILES) {
23
+ try {
24
+ accessSync(resolve(currentDir, file));
25
+ return wrapWithContextFile(currentDir);
26
+ } catch (e) {
27
+ // ignore
28
+ }
29
+ }
30
+ currentDir = resolve(currentDir, '..');
31
+ }
32
+
33
+ return wrapWithContextFile(cwd);
34
+ };
35
+
36
+ export const readContextFile = (file: string): Context => {
37
+ try {
38
+ return JSON.parse(readFileSync(file, 'utf-8'));
39
+ } catch (e) {
40
+ return {};
41
+ }
42
+ };
43
+
44
+ export const enrichFromContext = (
45
+ args: yargs.Arguments<{ contextFile: string }>,
46
+ ) => {
47
+ if (args._[0] === 'set-context') {
48
+ return;
49
+ }
50
+ const context = readContextFile(args.contextFile);
51
+ if (!args.branch && !args.id && !args.name) {
52
+ args.branch = context.branchId;
53
+ }
54
+ if (!args.projectId) {
55
+ args.projectId = context.projectId;
56
+ }
57
+ };
58
+
59
+ export const updateContextFile = (file: string, context: Context) => {
60
+ writeFileSync(file, JSON.stringify(context, null, 2));
61
+ };
package/src/env.ts ADDED
@@ -0,0 +1,7 @@
1
+ export const isCi = () => {
2
+ return process.env.CI !== 'false' && Boolean(process.env.CI);
3
+ };
4
+
5
+ export const isDebug = () => {
6
+ return Boolean(process.env.DEBUG);
7
+ };
package/src/errors.ts ADDED
@@ -0,0 +1,24 @@
1
+ export type ErrorCode =
2
+ | 'REQUEST_TIMEOUT'
3
+ | 'AUTH_FAILED'
4
+ | 'API_ERROR'
5
+ | 'UNKNOWN_COMMAND'
6
+ | 'MISSING_ARGUMENT'
7
+ | 'UNKNOWN_ERROR';
8
+
9
+ const ERROR_MATCHERS = [
10
+ [/^Unknown command: (.*)$/, 'UNKNOWN_COMMAND'],
11
+ [/^Missing required argument: (.*)$/, 'MISSING_ARGUMENT'],
12
+ ] as const;
13
+ export const matchErrorCode = (message?: string): ErrorCode => {
14
+ if (!message) {
15
+ return 'UNKNOWN_ERROR';
16
+ }
17
+ for (const [matcher, code] of ERROR_MATCHERS) {
18
+ const match = message.match(matcher);
19
+ if (match) {
20
+ return code;
21
+ }
22
+ }
23
+ return 'UNKNOWN_ERROR';
24
+ };
package/src/help.ts ADDED
@@ -0,0 +1,185 @@
1
+ import yargs from 'yargs';
2
+ import cliui from 'cliui';
3
+ import chalk from 'chalk';
4
+
5
+ import {
6
+ consumeBlockIfMatches,
7
+ consumeNextMatching,
8
+ drawPointer,
9
+ splitColumns,
10
+ } from './utils/ui.js';
11
+
12
+ // target width for the leftmost column
13
+ const SPACE_WIDTH = 20;
14
+
15
+ const formatHelp = (help: string) => {
16
+ const lines = help.split('\n');
17
+ const result = [] as string[];
18
+ // full command, like `neonctl projects list`
19
+ const topLevelCommand = consumeNextMatching(lines, /^.*/);
20
+
21
+ if (topLevelCommand) {
22
+ result.push(
23
+ chalk.bold(
24
+ topLevelCommand.replace('[options]', chalk.reset.green('[options]')),
25
+ ),
26
+ );
27
+ result.push('');
28
+ }
29
+
30
+ // commands description block
31
+ // example command to see: neonctl projects
32
+ const commandsBlock = consumeBlockIfMatches(lines, /^Commands:/);
33
+ if (commandsBlock.length > 0) {
34
+ const header = commandsBlock.shift() as string;
35
+ result.push(header);
36
+ const ui = cliui({
37
+ width: 0,
38
+ });
39
+ commandsBlock.forEach((line) => {
40
+ if (line.match(/^\s{3,}/)) {
41
+ ui.div(
42
+ {
43
+ text: '',
44
+ width: SPACE_WIDTH,
45
+ padding: [0, 0, 0, 0],
46
+ },
47
+ { text: line.trim(), padding: [0, 0, 0, 0] },
48
+ );
49
+ return;
50
+ }
51
+
52
+ const [command, description] = splitColumns(line);
53
+
54
+ // patch the previous command if it was multiline
55
+ if (!description && ui.rows.length > 1) {
56
+ ui.rows[ui.rows.length - 2][0].text += command;
57
+ return;
58
+ }
59
+
60
+ ui.div(chalk.cyan(command));
61
+ ui.div(
62
+ {
63
+ text: chalk.gray(drawPointer(SPACE_WIDTH)),
64
+ width: SPACE_WIDTH,
65
+ padding: [0, 0, 0, 0],
66
+ },
67
+ { text: description, padding: [0, 0, 0, 2] },
68
+ );
69
+ });
70
+ result.push(ui.toString());
71
+ result.push('');
72
+ }
73
+
74
+ // positional args block
75
+ // example command to see: neonctl branches rename
76
+ const positionalsBlock = consumeBlockIfMatches(lines, /Positionals:/);
77
+ if (positionalsBlock.length > 0) {
78
+ const header = positionalsBlock.shift() as string;
79
+ result.push(header);
80
+ const ui = cliui({
81
+ width: 0,
82
+ });
83
+ positionalsBlock.forEach((line) => {
84
+ const [positional, description] = splitColumns(line);
85
+ ui.div(
86
+ {
87
+ text: positional,
88
+ width: SPACE_WIDTH,
89
+ padding: [0, 2, 0, 0],
90
+ },
91
+ {
92
+ text: description,
93
+ padding: [0, 0, 0, 0],
94
+ },
95
+ );
96
+ });
97
+ result.push(ui.toString());
98
+ result.push('');
99
+ }
100
+
101
+ // command description
102
+ // example command to see: neonctl projects list
103
+ const descritpionBlock = consumeBlockIfMatches(lines, /^(?!.*options:)/i);
104
+ if (descritpionBlock.length > 0) {
105
+ result.push(...descritpionBlock);
106
+ result.push('');
107
+ }
108
+
109
+ while (true) {
110
+ // there are two options blocks: global and specific
111
+ // example to see both: neonctl projects create
112
+ const optionsBlock = consumeBlockIfMatches(lines, /.*options:/i);
113
+ if (optionsBlock.length === 0) {
114
+ break;
115
+ }
116
+ result.push(optionsBlock.shift() as string);
117
+ optionsBlock.forEach((line) => {
118
+ const [option, description] = splitColumns(line);
119
+ const ui = cliui({
120
+ width: 0,
121
+ });
122
+ if (option.startsWith('-')) {
123
+ ui.div({
124
+ text: chalk.green(option),
125
+ padding: [0, 0, 0, 0],
126
+ });
127
+ ui.div(
128
+ {
129
+ text: chalk.gray(drawPointer(SPACE_WIDTH)),
130
+ width: SPACE_WIDTH,
131
+ padding: [0, 2, 0, 0],
132
+ },
133
+ {
134
+ text: chalk.rgb(210, 210, 210)(description ?? ''),
135
+ padding: [0, 0, 0, 0],
136
+ },
137
+ );
138
+ } else {
139
+ ui.div(
140
+ {
141
+ padding: [0, 0, 0, 0],
142
+ text: '',
143
+ width: SPACE_WIDTH,
144
+ },
145
+ {
146
+ text: chalk.rgb(210, 210, 210)(option),
147
+ padding: [0, 0, 0, 0],
148
+ },
149
+ );
150
+ }
151
+
152
+ result.push(ui.toString());
153
+ });
154
+ result.push('');
155
+ }
156
+
157
+ const exampleBlock = consumeBlockIfMatches(lines, /Examples:/);
158
+ if (exampleBlock.length > 0) {
159
+ result.push(exampleBlock.shift() as string);
160
+ const ui = cliui({
161
+ width: 0,
162
+ });
163
+ for (const line of exampleBlock) {
164
+ const [command, description] = splitColumns(line);
165
+ ui.div({
166
+ text: chalk.bold(command),
167
+ padding: [0, 0, 0, 0],
168
+ });
169
+ ui.div({
170
+ text: chalk.reset(description),
171
+ padding: [0, 0, 0, 2],
172
+ });
173
+ }
174
+ result.push(ui.toString());
175
+ }
176
+
177
+ return [...result, ...lines];
178
+ };
179
+
180
+ export const showHelp = async (argv: yargs.Argv) => {
181
+ // add wrap to ensure that there are no line breaks
182
+ const help = await argv.getHelp();
183
+ process.stderr.write(formatHelp(help).join('\n') + '\n');
184
+ process.exit(0);
185
+ };