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,467 @@
1
+ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { dirname, join, parse, resolve } from 'node:path';
3
+ import { Injectable, Optional } from '@nestjs/common';
4
+ import {
5
+ AuthCredentials,
6
+ RevisiumUrlComplete,
7
+ UrlEnvConfig,
8
+ } from 'src/services/url';
9
+ import { CredentialStoreService } from 'src/services/credentials/credential-store.service';
10
+ import { UrlParserService } from 'src/services/url/url-parser.service';
11
+
12
+ export const WORKSPACE_CONFIG_DIR = '.revisium';
13
+ export const WORKSPACE_CONFIG_FILE = 'revisium-cli.config.json';
14
+ export const DEFAULT_BRANCH = 'master';
15
+ export const DEFAULT_REVISION = 'draft';
16
+ export const DEFAULT_CREDENTIAL = 'default';
17
+
18
+ export type WorkspaceAuthMode = 'none' | 'stored';
19
+
20
+ export interface WorkspaceInstanceConfig {
21
+ baseUrl: string;
22
+ authMode?: WorkspaceAuthMode;
23
+ }
24
+
25
+ export interface WorkspaceContextConfig {
26
+ instance: string;
27
+ credential?: string;
28
+ organization: string;
29
+ project: string;
30
+ branch?: string;
31
+ revision?: string;
32
+ }
33
+
34
+ export interface WorkspaceConfig {
35
+ version: 1;
36
+ currentContext?: string;
37
+ instances: Record<string, WorkspaceInstanceConfig>;
38
+ contexts: Record<string, WorkspaceContextConfig>;
39
+ }
40
+
41
+ export interface LoadedWorkspaceConfig {
42
+ path: string;
43
+ config: WorkspaceConfig;
44
+ }
45
+
46
+ export interface LoadedWorkspaceContext {
47
+ loaded: LoadedWorkspaceConfig;
48
+ context: WorkspaceContextConfig;
49
+ }
50
+
51
+ @Injectable()
52
+ export class WorkspaceConfigService {
53
+ constructor(
54
+ private readonly urlParser: UrlParserService,
55
+ @Optional() private readonly credentialStore?: CredentialStoreService,
56
+ ) {}
57
+
58
+ async load(
59
+ startDir = process.cwd(),
60
+ ): Promise<LoadedWorkspaceConfig | undefined> {
61
+ const configPath = await this.findConfigPath(startDir);
62
+ if (!configPath) {
63
+ return undefined;
64
+ }
65
+
66
+ const raw = await readFile(configPath, 'utf-8');
67
+ const parsed = JSON.parse(raw) as unknown;
68
+
69
+ return {
70
+ path: configPath,
71
+ config: this.normalizeConfig(parsed, configPath),
72
+ };
73
+ }
74
+
75
+ async loadOrCreate(startDir = process.cwd()): Promise<LoadedWorkspaceConfig> {
76
+ const loaded = await this.load(startDir);
77
+ if (loaded) {
78
+ return loaded;
79
+ }
80
+
81
+ return {
82
+ path: this.getDefaultConfigPath(startDir),
83
+ config: this.createEmptyConfig(),
84
+ };
85
+ }
86
+
87
+ async save(path: string, config: WorkspaceConfig): Promise<void> {
88
+ await mkdir(dirname(path), { recursive: true });
89
+ await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
90
+ }
91
+
92
+ async loadContext(
93
+ name: string,
94
+ startDir = process.cwd(),
95
+ ): Promise<LoadedWorkspaceContext> {
96
+ const loaded = await this.load(startDir);
97
+ if (!loaded || !Object.hasOwn(loaded.config.contexts, name)) {
98
+ throw new Error(`Revisium context "${name}" was not found`);
99
+ }
100
+
101
+ return {
102
+ loaded,
103
+ context: loaded.config.contexts[name],
104
+ };
105
+ }
106
+
107
+ async findConfigPath(startDir = process.cwd()): Promise<string | undefined> {
108
+ let currentDir = resolve(startDir);
109
+ const root = parse(currentDir).root;
110
+
111
+ while (true) {
112
+ const candidate = this.getDefaultConfigPath(currentDir);
113
+ if (await this.exists(candidate)) {
114
+ return candidate;
115
+ }
116
+
117
+ if (currentDir === root) {
118
+ return undefined;
119
+ }
120
+
121
+ currentDir = dirname(currentDir);
122
+ }
123
+ }
124
+
125
+ getDefaultConfigPath(startDir = process.cwd()): string {
126
+ return join(resolve(startDir), WORKSPACE_CONFIG_DIR, WORKSPACE_CONFIG_FILE);
127
+ }
128
+
129
+ createEmptyConfig(): WorkspaceConfig {
130
+ return {
131
+ version: 1,
132
+ instances: {},
133
+ contexts: {},
134
+ };
135
+ }
136
+
137
+ normalizeBaseUrl(input: string): string {
138
+ const trimmed = input.trim();
139
+ if (!trimmed) {
140
+ throw new Error('Instance URL cannot be empty');
141
+ }
142
+
143
+ if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
144
+ return this.normalizeHttpBaseUrl(trimmed, input);
145
+ }
146
+
147
+ const parsed = this.urlParser.parse(trimmed);
148
+ if (!parsed.baseUrl) {
149
+ throw new Error(
150
+ `Could not parse instance URL "${input}". Use revisium://host[:port]`,
151
+ );
152
+ }
153
+
154
+ return this.stripTrailingSlashes(parsed.baseUrl);
155
+ }
156
+
157
+ parseContextUrl(input: string): {
158
+ baseUrl: string;
159
+ organization: string;
160
+ project: string;
161
+ branch: string;
162
+ revision: string;
163
+ } {
164
+ const parsed = this.urlParser.parse(input);
165
+
166
+ if (!parsed.baseUrl || !parsed.organization || !parsed.project) {
167
+ throw new Error(
168
+ 'Context URL must include host, organization, and project: revisium://host/org/project/branch[:revision]',
169
+ );
170
+ }
171
+
172
+ return {
173
+ baseUrl: this.stripTrailingSlashes(parsed.baseUrl),
174
+ organization: parsed.organization,
175
+ project: parsed.project,
176
+ branch: parsed.branch || DEFAULT_BRANCH,
177
+ revision: parsed.revision || DEFAULT_REVISION,
178
+ };
179
+ }
180
+
181
+ findInstanceNameByBaseUrl(
182
+ config: WorkspaceConfig,
183
+ baseUrl: string,
184
+ ): string | undefined {
185
+ const matches = Object.entries(config.instances)
186
+ .filter(([, instance]) => instance.baseUrl === baseUrl)
187
+ .map(([name]) => name);
188
+
189
+ if (matches.length > 1) {
190
+ throw new Error(
191
+ `Multiple instances use ${baseUrl}. Pass --instance to choose one.`,
192
+ );
193
+ }
194
+
195
+ return matches[0];
196
+ }
197
+
198
+ resolveConnection(
199
+ loaded: LoadedWorkspaceConfig,
200
+ contextName: string | undefined,
201
+ env: UrlEnvConfig,
202
+ ): RevisiumUrlComplete {
203
+ const selectedContext = contextName || loaded.config.currentContext;
204
+
205
+ if (!selectedContext) {
206
+ throw new Error(
207
+ `No current Revisium context is configured in ${loaded.path}. Run: revisium context use <name>`,
208
+ );
209
+ }
210
+
211
+ const context = loaded.config.contexts[selectedContext];
212
+ if (!context) {
213
+ throw new Error(
214
+ `Revisium context "${selectedContext}" was not found in ${loaded.path}`,
215
+ );
216
+ }
217
+
218
+ const instance = loaded.config.instances[context.instance];
219
+ if (!instance) {
220
+ throw new Error(
221
+ `Revisium instance "${context.instance}" for context "${selectedContext}" was not found in ${loaded.path}`,
222
+ );
223
+ }
224
+
225
+ return {
226
+ baseUrl: instance.baseUrl,
227
+ auth: this.resolveAuth(instance, context, selectedContext, env),
228
+ organization: context.organization,
229
+ project: context.project,
230
+ branch: context.branch || DEFAULT_BRANCH,
231
+ revision: context.revision || DEFAULT_REVISION,
232
+ };
233
+ }
234
+
235
+ private resolveAuth(
236
+ instance: WorkspaceInstanceConfig,
237
+ context: WorkspaceContextConfig,
238
+ contextName: string,
239
+ env: UrlEnvConfig,
240
+ ): AuthCredentials {
241
+ const envAuth = this.resolveEnvAuth(env);
242
+ if (envAuth) {
243
+ return envAuth;
244
+ }
245
+
246
+ if ((instance.authMode || 'stored') === 'none') {
247
+ return { method: 'none' };
248
+ }
249
+
250
+ const credential = context.credential || DEFAULT_CREDENTIAL;
251
+ const credentialRef = {
252
+ baseUrl: instance.baseUrl,
253
+ credential,
254
+ };
255
+ const savedCredential = this.resolveSavedCredential(
256
+ credentialRef,
257
+ contextName,
258
+ context.instance,
259
+ );
260
+ if (savedCredential) {
261
+ return savedCredential;
262
+ }
263
+
264
+ throw new Error(
265
+ `No credentials found for context "${contextName}" credential "${credential}". ` +
266
+ `Run: revisium auth login --instance ${context.instance} --credential ${credential} --api-key. ` +
267
+ 'Or set REVISIUM_API_KEY=... / REVISIUM_TOKEN=..., or configure the instance with authMode "none" for local standalone.',
268
+ );
269
+ }
270
+
271
+ private resolveSavedCredential(
272
+ ref: {
273
+ baseUrl: string;
274
+ credential: string;
275
+ },
276
+ contextName: string,
277
+ instanceName: string,
278
+ ): AuthCredentials | undefined {
279
+ try {
280
+ return this.credentialStore?.getCredential(ref);
281
+ } catch (error) {
282
+ const message = error instanceof Error ? error.message : String(error);
283
+ if (message.startsWith('Could not read Revisium credential')) {
284
+ throw new Error(
285
+ `No credentials found for context "${contextName}" credential "${ref.credential}". ` +
286
+ `Could not read the OS credential store: ${message}. ` +
287
+ `Run: revisium auth login --instance ${instanceName} --credential ${ref.credential} --api-key. ` +
288
+ 'Or set REVISIUM_API_KEY=... / REVISIUM_TOKEN=... for this run.',
289
+ );
290
+ }
291
+ throw error;
292
+ }
293
+ }
294
+
295
+ private resolveEnvAuth(env: UrlEnvConfig): AuthCredentials | undefined {
296
+ const methods: string[] = [];
297
+
298
+ if (env.token) {
299
+ methods.push('token');
300
+ }
301
+ if (env.apikey) {
302
+ methods.push('apikey');
303
+ }
304
+ if (env.username || env.password) {
305
+ if (!env.username || !env.password) {
306
+ throw new Error(
307
+ 'Both REVISIUM_USERNAME and REVISIUM_PASSWORD are required for password authentication',
308
+ );
309
+ }
310
+ methods.push('credentials');
311
+ }
312
+
313
+ if (methods.length > 1) {
314
+ throw new Error(
315
+ `Multiple authentication methods specified: ${methods.join(', ')}. ` +
316
+ 'Use only one: token, apikey, or username/password',
317
+ );
318
+ }
319
+
320
+ if (env.token) {
321
+ return { method: 'token', token: env.token };
322
+ }
323
+ if (env.apikey) {
324
+ return { method: 'apikey', apikey: env.apikey };
325
+ }
326
+ if (env.username && env.password) {
327
+ return {
328
+ method: 'password',
329
+ username: env.username,
330
+ password: env.password,
331
+ };
332
+ }
333
+
334
+ return undefined;
335
+ }
336
+
337
+ private normalizeConfig(value: unknown, path: string): WorkspaceConfig {
338
+ if (!this.isObject(value)) {
339
+ throw new TypeError(
340
+ `Invalid Revisium CLI config at ${path}: expected object`,
341
+ );
342
+ }
343
+
344
+ const version = value.version;
345
+ if (version !== 1) {
346
+ throw new Error(
347
+ `Invalid Revisium CLI config at ${path}: unsupported version ${String(version)}`,
348
+ );
349
+ }
350
+
351
+ const instances = this.normalizeRecord<WorkspaceInstanceConfig>(
352
+ value.instances,
353
+ 'instances',
354
+ path,
355
+ );
356
+ const contexts = this.normalizeRecord<WorkspaceContextConfig>(
357
+ value.contexts,
358
+ 'contexts',
359
+ path,
360
+ );
361
+
362
+ for (const [name, instance] of Object.entries(instances)) {
363
+ if (!this.isObject(instance) || typeof instance.baseUrl !== 'string') {
364
+ throw new TypeError(
365
+ `Invalid Revisium CLI config at ${path}: instance "${name}" must include baseUrl`,
366
+ );
367
+ }
368
+ instance.baseUrl = this.normalizeBaseUrl(instance.baseUrl);
369
+ if (
370
+ instance.authMode !== undefined &&
371
+ instance.authMode !== 'none' &&
372
+ instance.authMode !== 'stored'
373
+ ) {
374
+ throw new Error(
375
+ `Invalid Revisium CLI config at ${path}: instance "${name}" has unsupported authMode`,
376
+ );
377
+ }
378
+ }
379
+
380
+ for (const [name, context] of Object.entries(contexts)) {
381
+ if (
382
+ !this.isObject(context) ||
383
+ typeof context.instance !== 'string' ||
384
+ typeof context.organization !== 'string' ||
385
+ typeof context.project !== 'string'
386
+ ) {
387
+ throw new TypeError(
388
+ `Invalid Revisium CLI config at ${path}: context "${name}" must include instance, organization, and project`,
389
+ );
390
+ }
391
+ }
392
+
393
+ return {
394
+ version: 1,
395
+ currentContext:
396
+ typeof value.currentContext === 'string'
397
+ ? value.currentContext
398
+ : undefined,
399
+ instances,
400
+ contexts,
401
+ };
402
+ }
403
+
404
+ private normalizeRecord<T>(
405
+ value: unknown,
406
+ key: string,
407
+ path: string,
408
+ ): Record<string, T> {
409
+ if (value === undefined) {
410
+ return {};
411
+ }
412
+ if (!this.isObject(value)) {
413
+ throw new TypeError(
414
+ `Invalid Revisium CLI config at ${path}: ${key} must be an object`,
415
+ );
416
+ }
417
+ return value as Record<string, T>;
418
+ }
419
+
420
+ private isObject(value: unknown): value is Record<string, unknown> {
421
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
422
+ }
423
+
424
+ private stripTrailingSlashes(value: string): string {
425
+ let endIndex = value.length;
426
+ while (endIndex > 0 && value[endIndex - 1] === '/') {
427
+ endIndex--;
428
+ }
429
+ return value.slice(0, endIndex);
430
+ }
431
+
432
+ private normalizeHttpBaseUrl(value: string, originalInput: string): string {
433
+ let parsed: URL;
434
+ try {
435
+ parsed = new URL(value);
436
+ } catch {
437
+ throw new Error(
438
+ `Could not parse instance URL "${originalInput}". Use http(s)://host[:port]`,
439
+ );
440
+ }
441
+
442
+ const hasPath = parsed.pathname !== '' && parsed.pathname !== '/';
443
+ if (
444
+ !parsed.hostname ||
445
+ parsed.username ||
446
+ parsed.password ||
447
+ hasPath ||
448
+ parsed.search ||
449
+ parsed.hash
450
+ ) {
451
+ throw new Error(
452
+ `Could not parse instance URL "${originalInput}". Use http(s)://host[:port]`,
453
+ );
454
+ }
455
+
456
+ return `${parsed.protocol}//${parsed.host}`;
457
+ }
458
+
459
+ private async exists(path: string): Promise<boolean> {
460
+ try {
461
+ await access(path);
462
+ return true;
463
+ } catch {
464
+ return false;
465
+ }
466
+ }
467
+ }
@@ -1,86 +0,0 @@
1
- name: Bump Version and Release
2
-
3
- on:
4
- workflow_dispatch:
5
- inputs:
6
- release_type:
7
- description: 'Select release type'
8
- required: true
9
- type: choice
10
- options:
11
- - patch
12
- - minor
13
- - major
14
-
15
- jobs:
16
- bump-and-tag:
17
- runs-on: ubuntu-latest
18
-
19
- steps:
20
- - name: Checkout repo without GITHUB_TOKEN credentials
21
- uses: actions/checkout@v4
22
- with:
23
- fetch-depth: 0
24
- persist-credentials: false
25
-
26
- - name: Verify branch and release type
27
- run: |
28
- BRANCH=$(git symbolic-ref --short HEAD)
29
- TYPE=${{ github.event.inputs.release_type }}
30
- echo "⎇ Running on branch: $BRANCH — release_type: $TYPE"
31
- if [[ "$BRANCH" == "master" ]]; then
32
- if [[ "$TYPE" != "minor" && "$TYPE" != "major" ]]; then
33
- echo "❌ Only 'minor' and 'major' releases are allowed on master!"
34
- exit 1
35
- fi
36
- elif [[ "$BRANCH" =~ ^release/[0-9]+\.[0-9]+\.x$ ]]; then
37
- if [[ "$TYPE" != "patch" ]]; then
38
- echo "❌ Only 'patch' releases are allowed on release branches!"
39
- exit 1
40
- fi
41
- else
42
- echo "❌ Workflow must run on 'master' or a 'release/X.Y.x' branch!"
43
- exit 1
44
- fi
45
-
46
- - name: Setup Git identity
47
- run: |
48
- git config user.name "anton62k"
49
- git config user.email "anton62k@users.noreply.github.com"
50
-
51
- - name: Bump version and push with PAT
52
- id: bump
53
- env:
54
- GH_PAT: ${{ secrets.PAT_TOKEN_FOR_BUMP_VERSION }}
55
- run: |
56
- # bump package.json, commit and tag
57
- NEW_TAG=$(npm version ${{ github.event.inputs.release_type }} -m "chore(release): %s")
58
- echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT
59
- echo "📦 New version: $NEW_TAG"
60
-
61
- # determine target branch: master or current release branch
62
- BRANCH=$(git symbolic-ref --short HEAD)
63
- if [[ "$BRANCH" == "master" ]]; then
64
- TARGET_BRANCH=master
65
- else
66
- TARGET_BRANCH=$BRANCH
67
- fi
68
-
69
- # push commit and tag to the correct branch
70
- git push https://x-access-token:$GH_PAT@github.com/${{ github.repository }} HEAD:${TARGET_BRANCH}
71
- git push https://x-access-token:$GH_PAT@github.com/${{ github.repository }} $NEW_TAG
72
-
73
- - name: Create release branch for minor and major bumps
74
- if: ${{ github.event.inputs.release_type == 'minor' || github.event.inputs.release_type == 'major' }}
75
- env:
76
- GH_PAT: ${{ secrets.PAT_TOKEN_FOR_BUMP_VERSION }}
77
- RAW_TAG: ${{ steps.bump.outputs.new_tag }}
78
- run: |
79
- # strip leading 'v' and drop patch to get "X.Y"
80
- VERSION_NUM=${RAW_TAG#v}
81
- MAJOR_MINOR=${VERSION_NUM%.*}
82
- RELEASE_BRANCH="release/${MAJOR_MINOR}.x"
83
-
84
- echo "🌱 Creating branch: $RELEASE_BRANCH"
85
- git branch "$RELEASE_BRANCH" HEAD
86
- git push https://x-access-token:$GH_PAT@github.com/${{ github.repository }} "$RELEASE_BRANCH"