revisium 2.3.0 → 2.5.0-alpha.0

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 (235) hide show
  1. package/.github/workflows/build.yml +11 -30
  2. package/.github/workflows/ci.yml +0 -6
  3. package/.github/workflows/npm-publish.yml +13 -78
  4. package/.github/workflows/release-train.yml +90 -0
  5. package/AGENTS.md +98 -0
  6. package/README.md +31 -13
  7. package/dist/e2e/utils/cli-runner.d.ts +2 -0
  8. package/dist/e2e/utils/cli-runner.js +14 -7
  9. package/dist/e2e/utils/cli-runner.js.map +1 -1
  10. package/dist/e2e/utils/matrix-fixtures.d.ts +26 -0
  11. package/dist/e2e/utils/matrix-fixtures.js +109 -0
  12. package/dist/e2e/utils/matrix-fixtures.js.map +1 -0
  13. package/dist/e2e/utils/matrix-workspace.d.ts +25 -0
  14. package/dist/e2e/utils/matrix-workspace.js +61 -0
  15. package/dist/e2e/utils/matrix-workspace.js.map +1 -0
  16. package/dist/e2e/utils/standalone-api.d.ts +53 -0
  17. package/dist/e2e/utils/standalone-api.js +146 -0
  18. package/dist/e2e/utils/standalone-api.js.map +1 -0
  19. package/dist/e2e/utils/standalone-runner.d.ts +23 -0
  20. package/dist/e2e/utils/standalone-runner.js +182 -0
  21. package/dist/e2e/utils/standalone-runner.js.map +1 -0
  22. package/dist/package.json +6 -3
  23. package/dist/src/app.module.js +51 -0
  24. package/dist/src/app.module.js.map +1 -1
  25. package/dist/src/commands/auth/auth-command.utils.d.ts +8 -0
  26. package/dist/src/commands/auth/auth-command.utils.js +28 -0
  27. package/dist/src/commands/auth/auth-command.utils.js.map +1 -0
  28. package/dist/src/commands/auth/auth-login.command.d.ts +29 -0
  29. package/dist/src/commands/auth/auth-login.command.js +155 -0
  30. package/dist/src/commands/auth/auth-login.command.js.map +1 -0
  31. package/dist/src/commands/auth/auth-logout.command.d.ts +19 -0
  32. package/dist/src/commands/auth/auth-logout.command.js +86 -0
  33. package/dist/src/commands/auth/auth-logout.command.js.map +1 -0
  34. package/dist/src/commands/auth/auth-status.command.d.ts +19 -0
  35. package/dist/src/commands/auth/auth-status.command.js +101 -0
  36. package/dist/src/commands/auth/auth-status.command.js.map +1 -0
  37. package/dist/src/commands/auth/auth.command.d.ts +5 -0
  38. package/dist/src/commands/auth/auth.command.js +35 -0
  39. package/dist/src/commands/auth/auth.command.js.map +1 -0
  40. package/dist/src/commands/base-sync.command.d.ts +2 -2
  41. package/dist/src/commands/base-sync.command.js +1 -2
  42. package/dist/src/commands/base-sync.command.js.map +1 -1
  43. package/dist/src/commands/base.command.d.ts +2 -0
  44. package/dist/src/commands/base.command.js +13 -0
  45. package/dist/src/commands/base.command.js.map +1 -1
  46. package/dist/src/commands/context/context-create.command.d.ts +28 -0
  47. package/dist/src/commands/context/context-create.command.js +172 -0
  48. package/dist/src/commands/context/context-create.command.js.map +1 -0
  49. package/dist/src/commands/context/context-list.command.d.ts +9 -0
  50. package/dist/src/commands/context/context-list.command.js +46 -0
  51. package/dist/src/commands/context/context-list.command.js.map +1 -0
  52. package/dist/src/commands/context/context-remove.command.d.ts +9 -0
  53. package/dist/src/commands/context/context-remove.command.js +48 -0
  54. package/dist/src/commands/context/context-remove.command.js.map +1 -0
  55. package/dist/src/commands/context/context-show.command.d.ts +9 -0
  56. package/dist/src/commands/context/context-show.command.js +64 -0
  57. package/dist/src/commands/context/context-show.command.js.map +1 -0
  58. package/dist/src/commands/context/context-use.command.d.ts +9 -0
  59. package/dist/src/commands/context/context-use.command.js +45 -0
  60. package/dist/src/commands/context/context-use.command.js.map +1 -0
  61. package/dist/src/commands/context/context.command.d.ts +5 -0
  62. package/dist/src/commands/context/context.command.js +43 -0
  63. package/dist/src/commands/context/context.command.js.map +1 -0
  64. package/dist/src/commands/endpoint/endpoint-ensure.command.d.ts +18 -0
  65. package/dist/src/commands/endpoint/endpoint-ensure.command.js +90 -0
  66. package/dist/src/commands/endpoint/endpoint-ensure.command.js.map +1 -0
  67. package/dist/src/commands/endpoint/endpoint-list.command.d.ts +14 -0
  68. package/dist/src/commands/endpoint/endpoint-list.command.js +62 -0
  69. package/dist/src/commands/endpoint/endpoint-list.command.js.map +1 -0
  70. package/dist/src/commands/endpoint/endpoint.command.d.ts +5 -0
  71. package/dist/src/commands/endpoint/endpoint.command.js +34 -0
  72. package/dist/src/commands/endpoint/endpoint.command.js.map +1 -0
  73. package/dist/src/commands/example/example-bootstrap.command.d.ts +24 -0
  74. package/dist/src/commands/example/example-bootstrap.command.js +133 -0
  75. package/dist/src/commands/example/example-bootstrap.command.js.map +1 -0
  76. package/dist/src/commands/example/example.command.d.ts +5 -0
  77. package/dist/src/commands/example/example.command.js +33 -0
  78. package/dist/src/commands/example/example.command.js.map +1 -0
  79. package/dist/src/commands/instance/instance-add.command.d.ts +16 -0
  80. package/dist/src/commands/instance/instance-add.command.js +83 -0
  81. package/dist/src/commands/instance/instance-add.command.js.map +1 -0
  82. package/dist/src/commands/instance/instance-list.command.d.ts +9 -0
  83. package/dist/src/commands/instance/instance-list.command.js +45 -0
  84. package/dist/src/commands/instance/instance-list.command.js.map +1 -0
  85. package/dist/src/commands/instance/instance-remove.command.d.ts +9 -0
  86. package/dist/src/commands/instance/instance-remove.command.js +54 -0
  87. package/dist/src/commands/instance/instance-remove.command.js.map +1 -0
  88. package/dist/src/commands/instance/instance-show.command.d.ts +9 -0
  89. package/dist/src/commands/instance/instance-show.command.js +50 -0
  90. package/dist/src/commands/instance/instance-show.command.js.map +1 -0
  91. package/dist/src/commands/instance/instance.command.d.ts +5 -0
  92. package/dist/src/commands/instance/instance.command.js +41 -0
  93. package/dist/src/commands/instance/instance.command.js.map +1 -0
  94. package/dist/src/commands/migration/apply-migrations.command.js +1 -0
  95. package/dist/src/commands/migration/apply-migrations.command.js.map +1 -1
  96. package/dist/src/commands/project/project-ensure.command.d.ts +16 -0
  97. package/dist/src/commands/project/project-ensure.command.js +71 -0
  98. package/dist/src/commands/project/project-ensure.command.js.map +1 -0
  99. package/dist/src/commands/project/project.command.d.ts +5 -0
  100. package/dist/src/commands/project/project.command.js +33 -0
  101. package/dist/src/commands/project/project.command.js.map +1 -0
  102. package/dist/src/services/bootstrap/bootstrap.service.d.ts +112 -0
  103. package/dist/src/services/bootstrap/bootstrap.service.js +438 -0
  104. package/dist/src/services/bootstrap/bootstrap.service.js.map +1 -0
  105. package/dist/src/services/bootstrap/index.d.ts +1 -0
  106. package/dist/src/services/bootstrap/index.js +6 -0
  107. package/dist/src/services/bootstrap/index.js.map +1 -0
  108. package/dist/src/services/connection/api-client.d.ts +4 -0
  109. package/dist/src/services/connection/api-client.js +67 -5
  110. package/dist/src/services/connection/api-client.js.map +1 -1
  111. package/dist/src/services/connection/connection-factory.service.js +4 -3
  112. package/dist/src/services/connection/connection-factory.service.js.map +1 -1
  113. package/dist/src/services/connection/connection.service.d.ts +10 -2
  114. package/dist/src/services/connection/connection.service.js +49 -4
  115. package/dist/src/services/connection/connection.service.js.map +1 -1
  116. package/dist/src/services/credentials/credential-store.service.d.ts +19 -0
  117. package/dist/src/services/credentials/credential-store.service.js +112 -0
  118. package/dist/src/services/credentials/credential-store.service.js.map +1 -0
  119. package/dist/src/services/credentials/credential-target.service.d.ts +22 -0
  120. package/dist/src/services/credentials/credential-target.service.js +107 -0
  121. package/dist/src/services/credentials/credential-target.service.js.map +1 -0
  122. package/dist/src/services/credentials/index.d.ts +2 -0
  123. package/dist/src/services/credentials/index.js +8 -0
  124. package/dist/src/services/credentials/index.js.map +1 -0
  125. package/dist/src/services/index.d.ts +3 -0
  126. package/dist/src/services/index.js +3 -0
  127. package/dist/src/services/index.js.map +1 -1
  128. package/dist/src/services/url/auth-prompt.service.d.ts +3 -1
  129. package/dist/src/services/url/auth-prompt.service.js +3 -0
  130. package/dist/src/services/url/auth-prompt.service.js.map +1 -1
  131. package/dist/src/services/url/url-builder.service.js +3 -0
  132. package/dist/src/services/url/url-builder.service.js.map +1 -1
  133. package/dist/src/services/url/url-parser.service.d.ts +1 -1
  134. package/dist/src/services/url/url-parser.service.js +12 -6
  135. package/dist/src/services/url/url-parser.service.js.map +1 -1
  136. package/dist/src/services/workspace/index.d.ts +1 -0
  137. package/dist/src/services/workspace/index.js +11 -0
  138. package/dist/src/services/workspace/index.js.map +1 -0
  139. package/dist/src/services/workspace/workspace-config.service.d.ts +66 -0
  140. package/dist/src/services/workspace/workspace-config.service.js +313 -0
  141. package/dist/src/services/workspace/workspace-config.service.js.map +1 -0
  142. package/dist/tsconfig.build.tsbuildinfo +1 -1
  143. package/docs/authentication.md +69 -26
  144. package/docs/bootstrap-commands.md +105 -0
  145. package/docs/configuration.md +49 -22
  146. package/docs/url-format.md +27 -2
  147. package/docs/workspace-config.md +135 -0
  148. package/e2e/jest-matrix.json +14 -0
  149. package/e2e/matrix/M01-auth-commands.e2e-spec.ts +241 -0
  150. package/e2e/matrix/M02-instance-commands.e2e-spec.ts +213 -0
  151. package/e2e/matrix/M03-context-commands.e2e-spec.ts +279 -0
  152. package/e2e/matrix/M04-project-ensure.e2e-spec.ts +218 -0
  153. package/e2e/matrix/M05-endpoint-commands.e2e-spec.ts +172 -0
  154. package/e2e/matrix/M06-example-bootstrap.e2e-spec.ts +437 -0
  155. package/e2e/matrix/M07-migrate.e2e-spec.ts +229 -0
  156. package/e2e/matrix/M08-schema.e2e-spec.ts +163 -0
  157. package/e2e/matrix/M09-rows.e2e-spec.ts +185 -0
  158. package/e2e/matrix/M10-sync.e2e-spec.ts +224 -0
  159. package/e2e/matrix/M11-target-resolution.e2e-spec.ts +177 -0
  160. package/e2e/matrix/M12-error-paths.e2e-spec.ts +162 -0
  161. package/e2e/matrix/M13-no-auth-mode.e2e-spec.ts +119 -0
  162. package/e2e/matrix/M14-multi-instance-workspace.e2e-spec.ts +182 -0
  163. package/e2e/matrix/README.md +354 -0
  164. package/e2e/tests/01-auth.e2e-spec.ts +19 -0
  165. package/e2e/tests/07-workspace-config.e2e-spec.ts +492 -0
  166. package/e2e/tests/08-bootstrap.e2e-spec.ts +304 -0
  167. package/e2e/utils/cli-runner.ts +27 -8
  168. package/e2e/utils/matrix-fixtures.ts +141 -0
  169. package/e2e/utils/matrix-workspace.ts +106 -0
  170. package/e2e/utils/standalone-api.ts +314 -0
  171. package/e2e/utils/standalone-runner.ts +276 -0
  172. package/package.json +6 -3
  173. package/src/app.module.ts +54 -0
  174. package/src/commands/auth/__tests__/auth-command.utils.spec.ts +41 -0
  175. package/src/commands/auth/__tests__/auth-login.command.spec.ts +131 -0
  176. package/src/commands/auth/__tests__/auth-logout.command.spec.ts +85 -0
  177. package/src/commands/auth/__tests__/auth-status.command.spec.ts +106 -0
  178. package/src/commands/auth/__tests__/auth.command.spec.ts +18 -0
  179. package/src/commands/auth/auth-command.utils.ts +36 -0
  180. package/src/commands/auth/auth-login.command.ts +146 -0
  181. package/src/commands/auth/auth-logout.command.ts +71 -0
  182. package/src/commands/auth/auth-status.command.ts +89 -0
  183. package/src/commands/auth/auth.command.ts +20 -0
  184. package/src/commands/base-sync.command.ts +2 -3
  185. package/src/commands/base.command.ts +11 -0
  186. package/src/commands/context/context-create.command.ts +173 -0
  187. package/src/commands/context/context-list.command.ts +36 -0
  188. package/src/commands/context/context-remove.command.ts +35 -0
  189. package/src/commands/context/context-show.command.ts +59 -0
  190. package/src/commands/context/context-use.command.ts +31 -0
  191. package/src/commands/context/context.command.ts +28 -0
  192. package/src/commands/endpoint/__tests__/endpoint-ensure.command.spec.ts +129 -0
  193. package/src/commands/endpoint/__tests__/endpoint-list.command.spec.ts +59 -0
  194. package/src/commands/endpoint/__tests__/endpoint.command.spec.ts +14 -0
  195. package/src/commands/endpoint/endpoint-ensure.command.ts +74 -0
  196. package/src/commands/endpoint/endpoint-list.command.ts +48 -0
  197. package/src/commands/endpoint/endpoint.command.ts +19 -0
  198. package/src/commands/example/__tests__/example-bootstrap.command.spec.ts +145 -0
  199. package/src/commands/example/__tests__/example.command.spec.ts +14 -0
  200. package/src/commands/example/example-bootstrap.command.ts +122 -0
  201. package/src/commands/example/example.command.ts +18 -0
  202. package/src/commands/instance/instance-add.command.ts +72 -0
  203. package/src/commands/instance/instance-list.command.ts +31 -0
  204. package/src/commands/instance/instance-remove.command.ts +44 -0
  205. package/src/commands/instance/instance-show.command.ts +35 -0
  206. package/src/commands/instance/instance.command.ts +26 -0
  207. package/src/commands/migration/apply-migrations.command.ts +1 -0
  208. package/src/commands/project/__tests__/project-ensure.command.spec.ts +119 -0
  209. package/src/commands/project/__tests__/project.command.spec.ts +14 -0
  210. package/src/commands/project/project-ensure.command.ts +61 -0
  211. package/src/commands/project/project.command.ts +18 -0
  212. package/src/services/bootstrap/__tests__/bootstrap.service.spec.ts +798 -0
  213. package/src/services/bootstrap/bootstrap.service.ts +698 -0
  214. package/src/services/bootstrap/index.ts +12 -0
  215. package/src/services/connection/__tests__/api-client.spec.ts +262 -0
  216. package/src/services/connection/__tests__/connection-factory.service.spec.ts +2 -0
  217. package/src/services/connection/__tests__/connection.service.spec.ts +161 -0
  218. package/src/services/connection/api-client.ts +88 -5
  219. package/src/services/connection/connection-factory.service.ts +4 -3
  220. package/src/services/connection/connection.service.ts +74 -3
  221. package/src/services/credentials/__tests__/credential-store.service.spec.ts +128 -0
  222. package/src/services/credentials/__tests__/credential-target.service.spec.ts +126 -0
  223. package/src/services/credentials/credential-store.service.ts +145 -0
  224. package/src/services/credentials/credential-target.service.ts +145 -0
  225. package/src/services/credentials/index.ts +9 -0
  226. package/src/services/index.ts +3 -0
  227. package/src/services/url/__tests__/url-builder.service.spec.ts +17 -0
  228. package/src/services/url/__tests__/url-parser.service.spec.ts +162 -0
  229. package/src/services/url/auth-prompt.service.ts +6 -1
  230. package/src/services/url/url-builder.service.ts +4 -0
  231. package/src/services/url/url-parser.service.ts +21 -6
  232. package/src/services/workspace/__tests__/workspace-config.service.spec.ts +378 -0
  233. package/src/services/workspace/index.ts +14 -0
  234. package/src/services/workspace/workspace-config.service.ts +467 -0
  235. package/.github/workflows/bump-version.yml +0 -86
@@ -0,0 +1,41 @@
1
+ import {
2
+ formatAuthLoginHint,
3
+ formatAuthRevisiumUrl,
4
+ formatAuthTarget,
5
+ } from '../auth-command.utils';
6
+
7
+ describe('auth command formatting helpers', () => {
8
+ it('formats instance and URL targets consistently', () => {
9
+ expect(formatAuthTarget('cloud', 'https://cloud.revisium.io')).toBe(
10
+ 'instance "cloud" (https://cloud.revisium.io)',
11
+ );
12
+ expect(formatAuthTarget(undefined, 'https://cloud.revisium.io')).toBe(
13
+ 'https://cloud.revisium.io',
14
+ );
15
+ });
16
+
17
+ it('formats login hints for instance and URL targets', () => {
18
+ expect(
19
+ formatAuthLoginHint({
20
+ instanceName: 'cloud',
21
+ baseUrl: 'https://cloud.revisium.io',
22
+ credential: 'admin',
23
+ }),
24
+ ).toBe("--instance 'cloud' --credential 'admin' --api-key");
25
+
26
+ expect(
27
+ formatAuthLoginHint({
28
+ baseUrl: 'https://cloud.revisium.io',
29
+ credential: 'default',
30
+ }),
31
+ ).toBe(
32
+ "--url 'revisium://cloud.revisium.io' --credential 'default' --api-key",
33
+ );
34
+ });
35
+
36
+ it('formats HTTP base URLs as revisium URLs', () => {
37
+ expect(formatAuthRevisiumUrl('http://localhost:9222')).toBe(
38
+ 'revisium+http://localhost:9222',
39
+ );
40
+ });
41
+ });
@@ -0,0 +1,131 @@
1
+ import { AuthLoginCommand } from '../auth-login.command';
2
+ import {
3
+ CredentialStoreService,
4
+ CredentialTargetService,
5
+ } from 'src/services/credentials';
6
+ import { InteractiveService, LoggerService } from 'src/services/common';
7
+
8
+ describe('AuthLoginCommand', () => {
9
+ let credentialTargets: { resolveRequired: jest.Mock };
10
+ let credentialStore: { saveApiKey: jest.Mock };
11
+ let interactive: { promptPassword: jest.Mock };
12
+ let logger: { success: jest.Mock };
13
+ let command: AuthLoginCommand;
14
+
15
+ beforeEach(() => {
16
+ credentialTargets = {
17
+ resolveRequired: jest.fn().mockResolvedValue({
18
+ baseUrl: 'https://cloud.revisium.io',
19
+ credential: 'admin',
20
+ instanceName: 'cloud',
21
+ authMode: 'stored',
22
+ }),
23
+ };
24
+ credentialStore = {
25
+ saveApiKey: jest.fn(),
26
+ };
27
+ interactive = {
28
+ promptPassword: jest.fn().mockResolvedValue(' rev_prompt '),
29
+ };
30
+ logger = {
31
+ success: jest.fn(),
32
+ };
33
+ command = new AuthLoginCommand(
34
+ credentialTargets as unknown as CredentialTargetService,
35
+ credentialStore as unknown as CredentialStoreService,
36
+ interactive as unknown as InteractiveService,
37
+ logger as unknown as LoggerService,
38
+ );
39
+ });
40
+
41
+ it('prompts for an API key and saves it for the resolved target', async () => {
42
+ await command.run([], {
43
+ instance: 'cloud',
44
+ credential: 'admin',
45
+ apiKey: true,
46
+ });
47
+
48
+ expect(interactive.promptPassword).toHaveBeenCalledWith(
49
+ 'Enter Revisium API key:',
50
+ );
51
+ expect(credentialStore.saveApiKey).toHaveBeenCalledWith(
52
+ {
53
+ baseUrl: 'https://cloud.revisium.io',
54
+ credential: 'admin',
55
+ },
56
+ 'rev_prompt',
57
+ );
58
+ expect(logger.success).toHaveBeenCalledWith(
59
+ 'Saved API key credential "admin" for instance "cloud" (https://cloud.revisium.io)',
60
+ );
61
+ });
62
+
63
+ it('rejects authMode none targets unless force is set', async () => {
64
+ credentialTargets.resolveRequired.mockResolvedValue({
65
+ baseUrl: 'http://localhost:9222',
66
+ credential: 'default',
67
+ instanceName: 'local',
68
+ authMode: 'none',
69
+ });
70
+
71
+ await expect(
72
+ command.run([], {
73
+ instance: 'local',
74
+ apiKey: true,
75
+ }),
76
+ ).rejects.toThrow('uses authMode "none"');
77
+
78
+ expect(credentialStore.saveApiKey).not.toHaveBeenCalled();
79
+ });
80
+
81
+ it('allows forced saves for authMode none targets', async () => {
82
+ credentialTargets.resolveRequired.mockResolvedValue({
83
+ baseUrl: 'http://localhost:9222',
84
+ credential: 'default',
85
+ instanceName: 'local',
86
+ authMode: 'none',
87
+ });
88
+
89
+ await command.run([], {
90
+ instance: 'local',
91
+ apiKey: true,
92
+ force: true,
93
+ });
94
+
95
+ expect(credentialStore.saveApiKey).toHaveBeenCalledWith(
96
+ {
97
+ baseUrl: 'http://localhost:9222',
98
+ credential: 'default',
99
+ },
100
+ 'rev_prompt',
101
+ );
102
+ });
103
+
104
+ it('requires one API key input mode', async () => {
105
+ await expect(
106
+ command.run([], {
107
+ instance: 'cloud',
108
+ }),
109
+ ).rejects.toThrow('Pass --api-key to prompt, or --api-key-stdin');
110
+
111
+ await expect(
112
+ command.run([], {
113
+ instance: 'cloud',
114
+ apiKey: true,
115
+ apiKeyStdin: true,
116
+ }),
117
+ ).rejects.toThrow('Use only one credential input');
118
+ });
119
+
120
+ it('parses command options', () => {
121
+ expect(command.parseUrl('revisium://cloud.revisium.io')).toBe(
122
+ 'revisium://cloud.revisium.io',
123
+ );
124
+ expect(command.parseInstance('cloud')).toBe('cloud');
125
+ expect(command.parseCredential('admin')).toBe('admin');
126
+ expect(command.parseApiKey()).toBe(true);
127
+ expect(command.parseApiKeyStdin()).toBe(true);
128
+ expect(command.parseForce()).toBe(true);
129
+ expect(command.parseForce('false')).toBe(false);
130
+ });
131
+ });
@@ -0,0 +1,85 @@
1
+ import { AuthLogoutCommand } from '../auth-logout.command';
2
+ import {
3
+ CredentialStoreService,
4
+ CredentialTargetService,
5
+ } from 'src/services/credentials';
6
+ import { LoggerService } from 'src/services/common';
7
+
8
+ describe('AuthLogoutCommand', () => {
9
+ let credentialTargets: { resolveOrCurrentContext: jest.Mock };
10
+ let credentialStore: { deleteCredential: jest.Mock };
11
+ let logger: {
12
+ success: jest.Mock;
13
+ warn: jest.Mock;
14
+ };
15
+ let command: AuthLogoutCommand;
16
+
17
+ beforeEach(() => {
18
+ credentialTargets = {
19
+ resolveOrCurrentContext: jest.fn().mockResolvedValue({
20
+ baseUrl: 'https://cloud.revisium.io',
21
+ credential: 'admin',
22
+ instanceName: 'cloud',
23
+ authMode: 'stored',
24
+ }),
25
+ };
26
+ credentialStore = {
27
+ deleteCredential: jest.fn().mockReturnValue(true),
28
+ };
29
+ logger = {
30
+ success: jest.fn(),
31
+ warn: jest.fn(),
32
+ };
33
+ command = new AuthLogoutCommand(
34
+ credentialTargets as unknown as CredentialTargetService,
35
+ credentialStore as unknown as CredentialStoreService,
36
+ logger as unknown as LoggerService,
37
+ );
38
+ });
39
+
40
+ it('deletes a saved credential for the resolved target', async () => {
41
+ await command.run([], {
42
+ instance: 'cloud',
43
+ credential: 'admin',
44
+ });
45
+
46
+ expect(credentialTargets.resolveOrCurrentContext).toHaveBeenCalledWith({
47
+ instance: 'cloud',
48
+ credential: 'admin',
49
+ });
50
+ expect(credentialStore.deleteCredential).toHaveBeenCalledWith({
51
+ baseUrl: 'https://cloud.revisium.io',
52
+ credential: 'admin',
53
+ });
54
+ expect(logger.success).toHaveBeenCalledWith(
55
+ 'Deleted saved credential "admin" for instance "cloud" (https://cloud.revisium.io)',
56
+ );
57
+ expect(logger.warn).not.toHaveBeenCalled();
58
+ });
59
+
60
+ it('warns when the resolved credential does not exist', async () => {
61
+ credentialTargets.resolveOrCurrentContext.mockResolvedValue({
62
+ baseUrl: 'https://cloud.revisium.io',
63
+ credential: 'default',
64
+ authMode: 'stored',
65
+ });
66
+ credentialStore.deleteCredential.mockReturnValue(false);
67
+
68
+ await command.run([], {
69
+ url: 'revisium://cloud.revisium.io',
70
+ });
71
+
72
+ expect(logger.warn).toHaveBeenCalledWith(
73
+ 'No saved credential "default" found for https://cloud.revisium.io',
74
+ );
75
+ expect(logger.success).not.toHaveBeenCalled();
76
+ });
77
+
78
+ it('parses command options', () => {
79
+ expect(command.parseUrl('revisium://cloud.revisium.io')).toBe(
80
+ 'revisium://cloud.revisium.io',
81
+ );
82
+ expect(command.parseInstance('cloud')).toBe('cloud');
83
+ expect(command.parseCredential('admin')).toBe('admin');
84
+ });
85
+ });
@@ -0,0 +1,106 @@
1
+ import { AuthStatusCommand } from '../auth-status.command';
2
+ import {
3
+ CredentialStoreService,
4
+ CredentialTargetService,
5
+ } from 'src/services/credentials';
6
+ import { LoggerService } from 'src/services/common';
7
+
8
+ describe('AuthStatusCommand', () => {
9
+ let credentialTargets: { resolveOrCurrentContext: jest.Mock };
10
+ let credentialStore: { hasCredential: jest.Mock };
11
+ let logger: {
12
+ info: jest.Mock;
13
+ success: jest.Mock;
14
+ warn: jest.Mock;
15
+ };
16
+ let command: AuthStatusCommand;
17
+
18
+ beforeEach(() => {
19
+ credentialTargets = {
20
+ resolveOrCurrentContext: jest.fn().mockResolvedValue({
21
+ baseUrl: 'https://cloud.revisium.io',
22
+ credential: 'admin',
23
+ instanceName: 'cloud',
24
+ contextName: 'dictionary',
25
+ authMode: 'stored',
26
+ }),
27
+ };
28
+ credentialStore = {
29
+ hasCredential: jest.fn().mockReturnValue(true),
30
+ };
31
+ logger = {
32
+ info: jest.fn(),
33
+ success: jest.fn(),
34
+ warn: jest.fn(),
35
+ };
36
+ command = new AuthStatusCommand(
37
+ credentialTargets as unknown as CredentialTargetService,
38
+ credentialStore as unknown as CredentialStoreService,
39
+ logger as unknown as LoggerService,
40
+ );
41
+ });
42
+
43
+ it('prints saved credential status for the current context', async () => {
44
+ await command.run([], {});
45
+
46
+ expect(credentialTargets.resolveOrCurrentContext).toHaveBeenCalledWith({});
47
+ expect(credentialStore.hasCredential).toHaveBeenCalledWith({
48
+ baseUrl: 'https://cloud.revisium.io',
49
+ credential: 'admin',
50
+ });
51
+ expect(logger.info).toHaveBeenCalledWith('Context: dictionary');
52
+ expect(logger.info).toHaveBeenCalledWith('Instance: cloud');
53
+ expect(logger.info).toHaveBeenCalledWith(
54
+ 'Base URL: https://cloud.revisium.io',
55
+ );
56
+ expect(logger.info).toHaveBeenCalledWith('Credential: admin');
57
+ expect(logger.info).toHaveBeenCalledWith('Auth mode: stored');
58
+ expect(logger.success).toHaveBeenCalledWith('Saved credential found');
59
+ expect(logger.warn).not.toHaveBeenCalled();
60
+ });
61
+
62
+ it('prints a login hint when the target has no saved credential', async () => {
63
+ credentialTargets.resolveOrCurrentContext.mockResolvedValue({
64
+ baseUrl: 'https://cloud.revisium.io',
65
+ credential: 'default',
66
+ authMode: 'stored',
67
+ });
68
+ credentialStore.hasCredential.mockReturnValue(false);
69
+
70
+ await command.run([], {
71
+ url: 'revisium://cloud.revisium.io/admin/dictionary/master',
72
+ });
73
+
74
+ expect(logger.warn).toHaveBeenCalledWith(
75
+ "No saved credential found. Run: revisium auth login --url 'revisium://cloud.revisium.io' --credential 'default' --api-key",
76
+ );
77
+ });
78
+
79
+ it('does not read the credential store for authMode none targets', async () => {
80
+ credentialTargets.resolveOrCurrentContext.mockResolvedValue({
81
+ baseUrl: 'http://localhost:9222',
82
+ credential: 'default',
83
+ instanceName: 'local',
84
+ authMode: 'none',
85
+ });
86
+
87
+ await command.run([], {
88
+ instance: 'local',
89
+ });
90
+
91
+ expect(logger.info).toHaveBeenCalledWith(
92
+ 'Saved credentials are bypassed for authMode "none".',
93
+ );
94
+ expect(credentialStore.hasCredential).not.toHaveBeenCalled();
95
+ expect(logger.success).not.toHaveBeenCalled();
96
+ expect(logger.warn).not.toHaveBeenCalled();
97
+ });
98
+
99
+ it('parses command options', () => {
100
+ expect(command.parseUrl('revisium://cloud.revisium.io')).toBe(
101
+ 'revisium://cloud.revisium.io',
102
+ );
103
+ expect(command.parseInstance('cloud')).toBe('cloud');
104
+ expect(command.parseCredential('admin')).toBe('admin');
105
+ });
106
+ });
@@ -0,0 +1,18 @@
1
+ import { AuthCommand } from '../auth.command';
2
+
3
+ describe('AuthCommand', () => {
4
+ it('shows help when no auth subcommand is provided', async () => {
5
+ const command = new AuthCommand();
6
+ const help = jest.fn();
7
+
8
+ Object.assign(command, {
9
+ command: {
10
+ help,
11
+ },
12
+ });
13
+
14
+ await command.run();
15
+
16
+ expect(help).toHaveBeenCalled();
17
+ });
18
+ });
@@ -0,0 +1,36 @@
1
+ export interface AuthCommandTarget {
2
+ instanceName?: string;
3
+ baseUrl: string;
4
+ credential: string;
5
+ }
6
+
7
+ function quoteArg(value: string): string {
8
+ if (process.platform === 'win32') {
9
+ return `"${value.replaceAll('"', '""')}"`;
10
+ }
11
+ const escaped = value.replaceAll("'", String.raw`'\''`);
12
+ return `'${escaped}'`;
13
+ }
14
+
15
+ export function formatAuthTarget(
16
+ instanceName: string | undefined,
17
+ baseUrl: string,
18
+ ): string {
19
+ return instanceName ? `instance "${instanceName}" (${baseUrl})` : baseUrl;
20
+ }
21
+
22
+ export function formatAuthLoginHint(target: AuthCommandTarget): string {
23
+ const targetSelector = target.instanceName
24
+ ? `--instance ${quoteArg(target.instanceName)}`
25
+ : `--url ${quoteArg(formatAuthRevisiumUrl(target.baseUrl))}`;
26
+
27
+ return `${targetSelector} --credential ${quoteArg(target.credential)} --api-key`;
28
+ }
29
+
30
+ export function formatAuthRevisiumUrl(baseUrl: string): string {
31
+ if (baseUrl.startsWith('http://')) {
32
+ return `revisium+http://${baseUrl.slice('http://'.length)}`;
33
+ }
34
+
35
+ return `revisium://${baseUrl.replace(/^https:\/\//, '')}`;
36
+ }
@@ -0,0 +1,146 @@
1
+ import { CommandRunner, Option, SubCommand } from 'nest-commander';
2
+ import {
3
+ CredentialStoreService,
4
+ CredentialTargetService,
5
+ } from 'src/services/credentials';
6
+ import { InteractiveService, LoggerService } from 'src/services/common';
7
+ import { parseBooleanOption } from 'src/utils/parse-boolean.utils';
8
+ import { formatAuthTarget } from './auth-command.utils';
9
+
10
+ interface Options {
11
+ url?: string;
12
+ instance?: string;
13
+ credential?: string;
14
+ apiKey?: boolean;
15
+ apiKeyStdin?: boolean;
16
+ force?: boolean;
17
+ }
18
+
19
+ @SubCommand({
20
+ name: 'login',
21
+ description: 'Save an API key in the OS credential store',
22
+ })
23
+ export class AuthLoginCommand extends CommandRunner {
24
+ constructor(
25
+ private readonly credentialTargets: CredentialTargetService,
26
+ private readonly credentialStore: CredentialStoreService,
27
+ private readonly interactive: InteractiveService,
28
+ private readonly logger: LoggerService,
29
+ ) {
30
+ super();
31
+ }
32
+
33
+ async run(_inputs: string[], options: Options): Promise<void> {
34
+ if (options.apiKey && options.apiKeyStdin) {
35
+ throw new Error(
36
+ 'Use only one credential input: --api-key or --api-key-stdin',
37
+ );
38
+ }
39
+ if (!options.apiKey && !options.apiKeyStdin) {
40
+ throw new Error(
41
+ 'Pass --api-key to prompt, or --api-key-stdin for scripts',
42
+ );
43
+ }
44
+
45
+ const target = await this.credentialTargets.resolveRequired(options);
46
+ if (target.authMode === 'none' && !options.force) {
47
+ throw new Error(
48
+ `Instance "${target.instanceName || target.baseUrl}" uses authMode "none". Pass --force to save credentials anyway.`,
49
+ );
50
+ }
51
+
52
+ const apiKey = options.apiKeyStdin
53
+ ? await this.readApiKeyFromStdin()
54
+ : await this.promptForApiKey();
55
+
56
+ this.credentialStore.saveApiKey(
57
+ {
58
+ baseUrl: target.baseUrl,
59
+ credential: target.credential,
60
+ },
61
+ apiKey,
62
+ );
63
+
64
+ this.logger.success(
65
+ `Saved API key credential "${target.credential}" for ${formatAuthTarget(target.instanceName, target.baseUrl)}`,
66
+ );
67
+ }
68
+
69
+ private async promptForApiKey(): Promise<string> {
70
+ const apiKey = await this.interactive.promptPassword(
71
+ 'Enter Revisium API key:',
72
+ );
73
+ return this.validateApiKey(apiKey);
74
+ }
75
+
76
+ private async readApiKeyFromStdin(): Promise<string> {
77
+ const chunks: Buffer[] = [];
78
+
79
+ for await (const chunk of process.stdin) {
80
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
81
+ if (Buffer.concat(chunks).includes(10)) {
82
+ break;
83
+ }
84
+ }
85
+
86
+ return this.validateApiKey(
87
+ Buffer.concat(chunks).toString('utf-8').split(/\r?\n/, 1)[0],
88
+ );
89
+ }
90
+
91
+ private validateApiKey(value: string | undefined): string {
92
+ const apiKey = value?.trim();
93
+ if (!apiKey) {
94
+ throw new Error('API key cannot be empty');
95
+ }
96
+ return apiKey;
97
+ }
98
+
99
+ @Option({
100
+ flags: '--url <url>',
101
+ description: 'Revisium server or target URL',
102
+ })
103
+ parseUrl(value: string) {
104
+ return value;
105
+ }
106
+
107
+ @Option({
108
+ flags: '--instance <name>',
109
+ description: 'Instance name from workspace config',
110
+ })
111
+ parseInstance(value: string) {
112
+ return value;
113
+ }
114
+
115
+ @Option({
116
+ flags: '--credential <name>',
117
+ description: 'Credential name (default: default)',
118
+ })
119
+ parseCredential(value: string) {
120
+ return value;
121
+ }
122
+
123
+ @Option({
124
+ flags: '--api-key',
125
+ description: 'Prompt for an API key and save it',
126
+ })
127
+ parseApiKey(): boolean {
128
+ return true;
129
+ }
130
+
131
+ @Option({
132
+ flags: '--api-key-stdin',
133
+ description: 'Read one API key line from stdin and save it',
134
+ })
135
+ parseApiKeyStdin(): boolean {
136
+ return true;
137
+ }
138
+
139
+ @Option({
140
+ flags: '--force [boolean]',
141
+ description: 'Allow saving credentials for an authMode none instance',
142
+ })
143
+ parseForce(value?: string): boolean {
144
+ return parseBooleanOption(value);
145
+ }
146
+ }
@@ -0,0 +1,71 @@
1
+ import { CommandRunner, Option, SubCommand } from 'nest-commander';
2
+ import {
3
+ CredentialStoreService,
4
+ CredentialTargetService,
5
+ } from 'src/services/credentials';
6
+ import { LoggerService } from 'src/services/common';
7
+ import { formatAuthTarget } from './auth-command.utils';
8
+
9
+ interface Options {
10
+ url?: string;
11
+ instance?: string;
12
+ credential?: string;
13
+ }
14
+
15
+ @SubCommand({
16
+ name: 'logout',
17
+ description: 'Delete a saved Revisium credential',
18
+ })
19
+ export class AuthLogoutCommand extends CommandRunner {
20
+ constructor(
21
+ private readonly credentialTargets: CredentialTargetService,
22
+ private readonly credentialStore: CredentialStoreService,
23
+ private readonly logger: LoggerService,
24
+ ) {
25
+ super();
26
+ }
27
+
28
+ async run(_inputs: string[], options: Options): Promise<void> {
29
+ const target =
30
+ await this.credentialTargets.resolveOrCurrentContext(options);
31
+ const deleted = this.credentialStore.deleteCredential({
32
+ baseUrl: target.baseUrl,
33
+ credential: target.credential,
34
+ });
35
+
36
+ if (deleted) {
37
+ this.logger.success(
38
+ `Deleted saved credential "${target.credential}" for ${formatAuthTarget(target.instanceName, target.baseUrl)}`,
39
+ );
40
+ return;
41
+ }
42
+
43
+ this.logger.warn(
44
+ `No saved credential "${target.credential}" found for ${formatAuthTarget(target.instanceName, target.baseUrl)}`,
45
+ );
46
+ }
47
+
48
+ @Option({
49
+ flags: '--url <url>',
50
+ description: 'Revisium server or target URL',
51
+ })
52
+ parseUrl(value: string) {
53
+ return value;
54
+ }
55
+
56
+ @Option({
57
+ flags: '--instance <name>',
58
+ description: 'Instance name from workspace config',
59
+ })
60
+ parseInstance(value: string) {
61
+ return value;
62
+ }
63
+
64
+ @Option({
65
+ flags: '--credential <name>',
66
+ description: 'Credential name',
67
+ })
68
+ parseCredential(value: string) {
69
+ return value;
70
+ }
71
+ }