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,162 @@
1
+ import { UrlParserService } from '../url-parser.service';
2
+
3
+ describe('UrlParserService', () => {
4
+ let service: UrlParserService;
5
+
6
+ beforeEach(() => {
7
+ service = new UrlParserService();
8
+ });
9
+
10
+ describe('parse - revisium+http://', () => {
11
+ it('forces HTTP for non-localhost host', () => {
12
+ const result = service.parse(
13
+ 'revisium+http://admin:pass@my-service:80/org/proj/branch',
14
+ );
15
+
16
+ expect(result).toEqual({
17
+ baseUrl: 'http://my-service:80',
18
+ username: 'admin',
19
+ password: 'pass',
20
+ token: undefined,
21
+ apikey: undefined,
22
+ organization: 'org',
23
+ project: 'proj',
24
+ branch: 'branch',
25
+ revision: undefined,
26
+ });
27
+ });
28
+
29
+ it('forces HTTP for non-localhost host with draft revision', () => {
30
+ const result = service.parse(
31
+ 'revisium+http://admin:pass@my-service:80/org/proj/branch:draft',
32
+ );
33
+
34
+ expect(result).toEqual({
35
+ baseUrl: 'http://my-service:80',
36
+ username: 'admin',
37
+ password: 'pass',
38
+ token: undefined,
39
+ apikey: undefined,
40
+ organization: 'org',
41
+ project: 'proj',
42
+ branch: 'branch',
43
+ revision: 'draft',
44
+ });
45
+ });
46
+
47
+ it('forces HTTP without port', () => {
48
+ const result = service.parse(
49
+ 'revisium+http://admin:pass@my-service/org/proj/branch',
50
+ );
51
+
52
+ expect(result).toEqual({
53
+ baseUrl: 'http://my-service',
54
+ username: 'admin',
55
+ password: 'pass',
56
+ token: undefined,
57
+ apikey: undefined,
58
+ organization: 'org',
59
+ project: 'proj',
60
+ branch: 'branch',
61
+ revision: undefined,
62
+ });
63
+ });
64
+
65
+ it('forces HTTP with token auth', () => {
66
+ const result = service.parse(
67
+ 'revisium+http://payment-svc:80/org/proj/master?token=abc123',
68
+ );
69
+
70
+ expect(result).toEqual({
71
+ baseUrl: 'http://payment-svc:80',
72
+ username: undefined,
73
+ password: undefined,
74
+ token: 'abc123',
75
+ apikey: undefined,
76
+ organization: 'org',
77
+ project: 'proj',
78
+ branch: 'master',
79
+ revision: undefined,
80
+ });
81
+ });
82
+ });
83
+
84
+ describe('parse - revisium+https://', () => {
85
+ it('forces HTTPS for any host', () => {
86
+ const result = service.parse(
87
+ 'revisium+https://admin:pass@my-service/org/proj/branch',
88
+ );
89
+
90
+ expect(result).toEqual({
91
+ baseUrl: 'https://my-service',
92
+ username: 'admin',
93
+ password: 'pass',
94
+ token: undefined,
95
+ apikey: undefined,
96
+ organization: 'org',
97
+ project: 'proj',
98
+ branch: 'branch',
99
+ revision: undefined,
100
+ });
101
+ });
102
+
103
+ it('forces HTTPS even for localhost', () => {
104
+ const result = service.parse(
105
+ 'revisium+https://admin:pass@localhost:8443/org/proj/main',
106
+ );
107
+
108
+ expect(result).toEqual({
109
+ baseUrl: 'https://localhost:8443',
110
+ username: 'admin',
111
+ password: 'pass',
112
+ token: undefined,
113
+ apikey: undefined,
114
+ organization: 'org',
115
+ project: 'proj',
116
+ branch: 'main',
117
+ revision: undefined,
118
+ });
119
+ });
120
+ });
121
+
122
+ describe('parse - revisium:// (unchanged behavior)', () => {
123
+ it('auto-detects http for localhost', () => {
124
+ const result = service.parse(
125
+ 'revisium://admin:pass@localhost:8888/org/proj/branch',
126
+ );
127
+
128
+ expect(result.baseUrl).toBe('http://localhost:8888');
129
+ });
130
+
131
+ it('auto-detects https for remote host', () => {
132
+ const result = service.parse(
133
+ 'revisium://admin:pass@cloud.revisium.io/org/proj/branch',
134
+ );
135
+
136
+ expect(result.baseUrl).toBe('https://cloud.revisium.io');
137
+ });
138
+ });
139
+
140
+ describe('buildBaseUrlFromHost - protocolOverride', () => {
141
+ it('uses override when provided', () => {
142
+ expect(service.buildBaseUrlFromHost('my-service:80', 'http')).toBe(
143
+ 'http://my-service:80',
144
+ );
145
+ });
146
+
147
+ it('override takes precedence over localhost auto-detect', () => {
148
+ expect(service.buildBaseUrlFromHost('localhost:8080', 'https')).toBe(
149
+ 'https://localhost:8080',
150
+ );
151
+ });
152
+
153
+ it('falls back to auto-detect when no override', () => {
154
+ expect(service.buildBaseUrlFromHost('localhost:8080')).toBe(
155
+ 'http://localhost:8080',
156
+ );
157
+ expect(service.buildBaseUrlFromHost('cloud.revisium.io')).toBe(
158
+ 'https://cloud.revisium.io',
159
+ );
160
+ });
161
+ });
162
+ });
@@ -1,9 +1,10 @@
1
1
  import { Injectable } from '@nestjs/common';
2
2
  import { InteractiveService } from '../common';
3
3
 
4
- export type AuthMethod = 'token' | 'apikey' | 'password';
4
+ export type AuthMethod = 'none' | 'token' | 'apikey' | 'password';
5
5
 
6
6
  export type AuthCredentials =
7
+ | { method: 'none' }
7
8
  | { method: 'token'; token: string }
8
9
  | { method: 'apikey'; apikey: string }
9
10
  | { method: 'password'; username: string; password: string };
@@ -27,6 +28,10 @@ export class AuthPromptService {
27
28
  ],
28
29
  );
29
30
 
31
+ if (authMethod === 'none') {
32
+ return { method: 'none' };
33
+ }
34
+
30
35
  if (authMethod === 'token') {
31
36
  const token = await this.interactive.promptPassword(
32
37
  `[${label}] Paste token:`,
@@ -46,6 +46,10 @@ export class UrlBuilderService {
46
46
 
47
47
  const basePath = `revisium://${baseUrlWithoutProtocol}/${url.organization}/${url.project}${branchPart}`;
48
48
 
49
+ if (url.auth.method === 'none') {
50
+ return basePath;
51
+ }
52
+
49
53
  if (url.auth.method === 'token') {
50
54
  const tokenValue = maskSecrets ? '****' : url.auth.token;
51
55
  return `${basePath}?token=${tokenValue}`;
@@ -27,6 +27,14 @@ function validatePort(port: number, context: string): void {
27
27
  @Injectable()
28
28
  export class UrlParserService {
29
29
  parse(input: string): RevisiumUrl {
30
+ if (input.startsWith('revisium+http://')) {
31
+ return this.parseRevisiumUrl(input, 'http');
32
+ }
33
+
34
+ if (input.startsWith('revisium+https://')) {
35
+ return this.parseRevisiumUrl(input, 'https');
36
+ }
37
+
30
38
  if (input.startsWith('revisium://')) {
31
39
  return this.parseRevisiumUrl(input);
32
40
  }
@@ -39,7 +47,10 @@ export class UrlParserService {
39
47
  return `${protocol}://${host}${portSuffix}`;
40
48
  }
41
49
 
42
- buildBaseUrlFromHost(hostWithPort: string): string {
50
+ buildBaseUrlFromHost(
51
+ hostWithPort: string,
52
+ protocolOverride?: 'http' | 'https',
53
+ ): string {
43
54
  const [host, portStr] = hostWithPort.split(':');
44
55
  const port = portStr ? Number.parseInt(portStr, 10) : undefined;
45
56
 
@@ -47,8 +58,9 @@ export class UrlParserService {
47
58
  validatePort(port, `in "${hostWithPort}"`);
48
59
  }
49
60
 
50
- const isLocalhost = LOCALHOST_HOSTS.has(host.toLowerCase());
51
- const protocol = isLocalhost ? 'http' : 'https';
61
+ const protocol =
62
+ protocolOverride ??
63
+ (LOCALHOST_HOSTS.has(host.toLowerCase()) ? 'http' : 'https');
52
64
 
53
65
  if (port) {
54
66
  return this.buildBaseUrl(protocol, host, port);
@@ -65,8 +77,11 @@ export class UrlParserService {
65
77
  validatePort(port, context);
66
78
  }
67
79
 
68
- private parseRevisiumUrl(url: string): RevisiumUrl {
69
- const withoutProtocol = url.replace('revisium://', '');
80
+ private parseRevisiumUrl(
81
+ url: string,
82
+ forceProtocol?: 'http' | 'https',
83
+ ): RevisiumUrl {
84
+ const withoutProtocol = url.replace(/^revisium(\+https?)?:\/\//, '');
70
85
 
71
86
  const [mainPart, queryString] = withoutProtocol.split('?');
72
87
  const queryParams = this.parseQueryParams(queryString);
@@ -104,7 +119,7 @@ export class UrlParserService {
104
119
  const pathParts = hostAndPath.split('/');
105
120
  const hostWithPort = pathParts[0];
106
121
 
107
- const baseUrl = this.buildBaseUrlFromHost(hostWithPort);
122
+ const baseUrl = this.buildBaseUrlFromHost(hostWithPort, forceProtocol);
108
123
 
109
124
  const branchWithRevision = pathParts[3] || undefined;
110
125
  let branch: string | undefined;
@@ -0,0 +1,378 @@
1
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ import { WorkspaceConfigService } from '../workspace-config.service';
5
+ import { UrlParserService } from '../../url/url-parser.service';
6
+ import { CredentialStoreService } from '../../credentials';
7
+
8
+ describe('WorkspaceConfigService', () => {
9
+ let service: WorkspaceConfigService;
10
+ let tempDir: string;
11
+
12
+ beforeEach(async () => {
13
+ service = new WorkspaceConfigService(new UrlParserService());
14
+ tempDir = await mkdtemp(join(tmpdir(), 'revisium-cli-workspace-'));
15
+ });
16
+
17
+ afterEach(async () => {
18
+ await rm(tempDir, { recursive: true, force: true });
19
+ });
20
+
21
+ it('writes and discovers the nearest workspace config from a child folder', async () => {
22
+ const loaded = await service.loadOrCreate(tempDir);
23
+ loaded.config.instances.local = {
24
+ baseUrl: 'http://localhost:9222',
25
+ authMode: 'none',
26
+ };
27
+ await service.save(loaded.path, loaded.config);
28
+
29
+ const childDir = join(tempDir, 'examples', 'dictionary');
30
+ await mkdir(childDir, { recursive: true });
31
+
32
+ const discovered = await service.load(childDir);
33
+
34
+ expect(discovered?.path).toBe(loaded.path);
35
+ expect(discovered?.config.instances.local).toEqual({
36
+ baseUrl: 'http://localhost:9222',
37
+ authMode: 'none',
38
+ });
39
+ });
40
+
41
+ it('normalizes revisium URLs to server base URLs', () => {
42
+ expect(
43
+ service.normalizeBaseUrl('revisium://localhost:9222/admin/proj'),
44
+ ).toBe('http://localhost:9222');
45
+ expect(service.normalizeBaseUrl('https://cloud.revisium.io/')).toBe(
46
+ 'https://cloud.revisium.io',
47
+ );
48
+ });
49
+
50
+ it('rejects direct http instance URLs with missing host or extra target path', () => {
51
+ expect(() => service.normalizeBaseUrl('https://')).toThrow(
52
+ 'Use http(s)://host[:port]',
53
+ );
54
+ expect(() =>
55
+ service.normalizeBaseUrl('https://cloud.revisium.io/admin/dictionary'),
56
+ ).toThrow('Use http(s)://host[:port]');
57
+ expect(() =>
58
+ service.normalizeBaseUrl('https://cloud.revisium.io?token=secret'),
59
+ ).toThrow('Use http(s)://host[:port]');
60
+ });
61
+
62
+ it('parses context URLs with default branch and revision', () => {
63
+ expect(
64
+ service.parseContextUrl('revisium://localhost:9222/admin/dictionary'),
65
+ ).toEqual({
66
+ baseUrl: 'http://localhost:9222',
67
+ organization: 'admin',
68
+ project: 'dictionary',
69
+ branch: 'master',
70
+ revision: 'draft',
71
+ });
72
+ });
73
+
74
+ it('resolves no-auth workspace contexts', () => {
75
+ const url = service.resolveConnection(
76
+ {
77
+ path: join(tempDir, '.revisium', 'revisium-cli.config.json'),
78
+ config: {
79
+ version: 1,
80
+ currentContext: 'dictionary-local',
81
+ instances: {
82
+ local: {
83
+ baseUrl: 'http://localhost:9222',
84
+ authMode: 'none',
85
+ },
86
+ },
87
+ contexts: {
88
+ 'dictionary-local': {
89
+ instance: 'local',
90
+ organization: 'admin',
91
+ project: 'dictionary',
92
+ },
93
+ },
94
+ },
95
+ },
96
+ undefined,
97
+ {},
98
+ );
99
+
100
+ expect(url).toEqual({
101
+ baseUrl: 'http://localhost:9222',
102
+ auth: { method: 'none' },
103
+ organization: 'admin',
104
+ project: 'dictionary',
105
+ branch: 'master',
106
+ revision: 'draft',
107
+ });
108
+ });
109
+
110
+ it('loads an existing context with its config path', async () => {
111
+ const loaded = await service.loadOrCreate(tempDir);
112
+ loaded.config.contexts.demo = {
113
+ instance: 'local',
114
+ organization: 'admin',
115
+ project: 'dictionary',
116
+ };
117
+ await service.save(loaded.path, loaded.config);
118
+
119
+ const result = await service.loadContext('demo', tempDir);
120
+
121
+ expect(result.loaded.path).toBe(loaded.path);
122
+ expect(result.context.project).toBe('dictionary');
123
+ });
124
+
125
+ it('fails when loading a missing context', async () => {
126
+ const loaded = await service.loadOrCreate(tempDir);
127
+ await service.save(loaded.path, loaded.config);
128
+
129
+ await expect(service.loadContext('missing', tempDir)).rejects.toThrow(
130
+ 'Revisium context "missing" was not found',
131
+ );
132
+ });
133
+
134
+ it('lets environment credentials override no-auth mode', () => {
135
+ const url = service.resolveConnection(
136
+ {
137
+ path: join(tempDir, '.revisium', 'revisium-cli.config.json'),
138
+ config: {
139
+ version: 1,
140
+ currentContext: 'dictionary-local',
141
+ instances: {
142
+ local: {
143
+ baseUrl: 'http://localhost:9222',
144
+ authMode: 'none',
145
+ },
146
+ },
147
+ contexts: {
148
+ 'dictionary-local': {
149
+ instance: 'local',
150
+ organization: 'admin',
151
+ project: 'dictionary',
152
+ },
153
+ },
154
+ },
155
+ },
156
+ undefined,
157
+ { apikey: 'rev_test' },
158
+ );
159
+
160
+ expect(url.auth).toEqual({ method: 'apikey', apikey: 'rev_test' });
161
+ });
162
+
163
+ it('resolves saved credentials for stored workspace contexts', () => {
164
+ const credentialStore = {
165
+ getCredential: jest
166
+ .fn()
167
+ .mockReturnValue({ method: 'apikey', apikey: 'rev_saved' }),
168
+ } as unknown as CredentialStoreService;
169
+ service = new WorkspaceConfigService(
170
+ new UrlParserService(),
171
+ credentialStore,
172
+ );
173
+
174
+ const url = service.resolveConnection(
175
+ {
176
+ path: join(tempDir, '.revisium', 'revisium-cli.config.json'),
177
+ config: {
178
+ version: 1,
179
+ currentContext: 'cloud',
180
+ instances: {
181
+ cloud: {
182
+ baseUrl: 'https://cloud.revisium.io',
183
+ authMode: 'stored',
184
+ },
185
+ },
186
+ contexts: {
187
+ cloud: {
188
+ instance: 'cloud',
189
+ credential: 'admin',
190
+ organization: 'admin',
191
+ project: 'dictionary',
192
+ },
193
+ },
194
+ },
195
+ },
196
+ undefined,
197
+ {},
198
+ );
199
+
200
+ expect(url.auth).toEqual({ method: 'apikey', apikey: 'rev_saved' });
201
+ expect(credentialStore.getCredential).toHaveBeenCalledWith({
202
+ baseUrl: 'https://cloud.revisium.io',
203
+ credential: 'admin',
204
+ });
205
+ });
206
+
207
+ it('lets environment credentials override saved credentials', () => {
208
+ const credentialStore = {
209
+ getCredential: jest.fn(),
210
+ } as unknown as CredentialStoreService;
211
+ service = new WorkspaceConfigService(
212
+ new UrlParserService(),
213
+ credentialStore,
214
+ );
215
+
216
+ const url = service.resolveConnection(
217
+ {
218
+ path: join(tempDir, '.revisium', 'revisium-cli.config.json'),
219
+ config: {
220
+ version: 1,
221
+ currentContext: 'cloud',
222
+ instances: {
223
+ cloud: {
224
+ baseUrl: 'https://cloud.revisium.io',
225
+ authMode: 'stored',
226
+ },
227
+ },
228
+ contexts: {
229
+ cloud: {
230
+ instance: 'cloud',
231
+ credential: 'admin',
232
+ organization: 'admin',
233
+ project: 'dictionary',
234
+ },
235
+ },
236
+ },
237
+ },
238
+ undefined,
239
+ { apikey: 'rev_env' },
240
+ );
241
+
242
+ expect(url.auth).toEqual({ method: 'apikey', apikey: 'rev_env' });
243
+ expect(credentialStore.getCredential).not.toHaveBeenCalled();
244
+ });
245
+
246
+ it('fails with remediation when stored credentials cannot be read', () => {
247
+ const credentialStore = {
248
+ getCredential: jest.fn(() => {
249
+ throw new Error(
250
+ 'Could not read Revisium credential "admin" for https://cloud.revisium.io: keyring unavailable',
251
+ );
252
+ }),
253
+ } as unknown as CredentialStoreService;
254
+ service = new WorkspaceConfigService(
255
+ new UrlParserService(),
256
+ credentialStore,
257
+ );
258
+
259
+ expect(() =>
260
+ service.resolveConnection(
261
+ {
262
+ path: join(tempDir, '.revisium', 'revisium-cli.config.json'),
263
+ config: {
264
+ version: 1,
265
+ currentContext: 'cloud',
266
+ instances: {
267
+ cloud: {
268
+ baseUrl: 'https://cloud.revisium.io',
269
+ authMode: 'stored',
270
+ },
271
+ },
272
+ contexts: {
273
+ cloud: {
274
+ instance: 'cloud',
275
+ credential: 'admin',
276
+ organization: 'admin',
277
+ project: 'dictionary',
278
+ },
279
+ },
280
+ },
281
+ },
282
+ undefined,
283
+ {},
284
+ ),
285
+ ).toThrow(
286
+ 'No credentials found for context "cloud" credential "admin". Could not read the OS credential store',
287
+ );
288
+ });
289
+
290
+ it('fails with remediation when stored credentials are required', () => {
291
+ expect(() =>
292
+ service.resolveConnection(
293
+ {
294
+ path: join(tempDir, '.revisium', 'revisium-cli.config.json'),
295
+ config: {
296
+ version: 1,
297
+ currentContext: 'cloud',
298
+ instances: {
299
+ cloud: {
300
+ baseUrl: 'https://cloud.revisium.io',
301
+ authMode: 'stored',
302
+ },
303
+ },
304
+ contexts: {
305
+ cloud: {
306
+ instance: 'cloud',
307
+ credential: 'admin',
308
+ organization: 'admin',
309
+ project: 'dictionary',
310
+ },
311
+ },
312
+ },
313
+ },
314
+ undefined,
315
+ {},
316
+ ),
317
+ ).toThrow(
318
+ 'No credentials found for context "cloud" credential "admin". Run: revisium auth login --instance cloud --credential admin --api-key.',
319
+ );
320
+ });
321
+
322
+ it('rejects malformed config files', async () => {
323
+ const configPath = service.getDefaultConfigPath(tempDir);
324
+ await mkdir(join(tempDir, '.revisium'), { recursive: true });
325
+ await writeFile(configPath, '{"version":2}', 'utf-8');
326
+
327
+ await expect(service.load(tempDir)).rejects.toThrow(
328
+ 'unsupported version 2',
329
+ );
330
+ });
331
+
332
+ it('normalizes instance base URLs when loading config files', async () => {
333
+ const configPath = service.getDefaultConfigPath(tempDir);
334
+ await mkdir(join(tempDir, '.revisium'), { recursive: true });
335
+ await writeFile(
336
+ configPath,
337
+ JSON.stringify({
338
+ version: 1,
339
+ instances: {
340
+ cloud: {
341
+ baseUrl: 'https://cloud.revisium.io/',
342
+ authMode: 'stored',
343
+ },
344
+ },
345
+ contexts: {},
346
+ }),
347
+ 'utf-8',
348
+ );
349
+
350
+ const loaded = await service.load(tempDir);
351
+
352
+ expect(loaded?.config.instances.cloud.baseUrl).toBe(
353
+ 'https://cloud.revisium.io',
354
+ );
355
+ });
356
+
357
+ it('writes only the workspace config file when saving', async () => {
358
+ const loaded = await service.loadOrCreate(tempDir);
359
+ loaded.config.instances.local = {
360
+ baseUrl: 'http://localhost:9222',
361
+ authMode: 'none',
362
+ };
363
+
364
+ await service.save(loaded.path, loaded.config);
365
+
366
+ const raw = await readFile(loaded.path, 'utf-8');
367
+ expect(JSON.parse(raw)).toEqual({
368
+ version: 1,
369
+ instances: {
370
+ local: {
371
+ baseUrl: 'http://localhost:9222',
372
+ authMode: 'none',
373
+ },
374
+ },
375
+ contexts: {},
376
+ });
377
+ });
378
+ });
@@ -0,0 +1,14 @@
1
+ export {
2
+ DEFAULT_BRANCH,
3
+ DEFAULT_CREDENTIAL,
4
+ DEFAULT_REVISION,
5
+ LoadedWorkspaceContext,
6
+ LoadedWorkspaceConfig,
7
+ WORKSPACE_CONFIG_DIR,
8
+ WORKSPACE_CONFIG_FILE,
9
+ WorkspaceAuthMode,
10
+ WorkspaceConfig,
11
+ WorkspaceConfigService,
12
+ WorkspaceContextConfig,
13
+ WorkspaceInstanceConfig,
14
+ } from './workspace-config.service';