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,89 @@
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 { formatAuthLoginHint } from './auth-command.utils';
8
+
9
+ interface Options {
10
+ url?: string;
11
+ instance?: string;
12
+ credential?: string;
13
+ }
14
+
15
+ @SubCommand({
16
+ name: 'status',
17
+ description: 'Show saved Revisium credential status',
18
+ })
19
+ export class AuthStatusCommand 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 ref = {
32
+ baseUrl: target.baseUrl,
33
+ credential: target.credential,
34
+ };
35
+
36
+ if (target.contextName) {
37
+ this.logger.info(`Context: ${target.contextName}`);
38
+ }
39
+ if (target.instanceName) {
40
+ this.logger.info(`Instance: ${target.instanceName}`);
41
+ }
42
+ this.logger.info(`Base URL: ${target.baseUrl}`);
43
+ this.logger.info(`Credential: ${target.credential}`);
44
+ this.logger.info(`Auth mode: ${target.authMode || 'stored'}`);
45
+
46
+ if ((target.authMode || 'stored') === 'none') {
47
+ this.logger.info('Saved credentials are bypassed for authMode "none".');
48
+ return;
49
+ }
50
+
51
+ const hasCredential = this.credentialStore.hasCredential(ref);
52
+
53
+ if (hasCredential) {
54
+ this.logger.success('Saved credential found');
55
+ this.logger.info(
56
+ 'API key identity verification is limited until auth principal introspection is available.',
57
+ );
58
+ return;
59
+ }
60
+
61
+ this.logger.warn(
62
+ `No saved credential found. Run: revisium auth login ${formatAuthLoginHint(target)}`,
63
+ );
64
+ }
65
+
66
+ @Option({
67
+ flags: '--url <url>',
68
+ description: 'Revisium server or target URL',
69
+ })
70
+ parseUrl(value: string) {
71
+ return value;
72
+ }
73
+
74
+ @Option({
75
+ flags: '--instance <name>',
76
+ description: 'Instance name from workspace config',
77
+ })
78
+ parseInstance(value: string) {
79
+ return value;
80
+ }
81
+
82
+ @Option({
83
+ flags: '--credential <name>',
84
+ description: 'Credential name',
85
+ })
86
+ parseCredential(value: string) {
87
+ return value;
88
+ }
89
+ }
@@ -0,0 +1,20 @@
1
+ import { Command, CommandRunner } from 'nest-commander';
2
+ import { AuthLoginCommand } from './auth-login.command';
3
+ import { AuthLogoutCommand } from './auth-logout.command';
4
+ import { AuthStatusCommand } from './auth-status.command';
5
+
6
+ @Command({
7
+ name: 'auth',
8
+ description: 'Manage saved Revisium credentials',
9
+ subCommands: [AuthLoginCommand, AuthStatusCommand, AuthLogoutCommand],
10
+ })
11
+ export class AuthCommand extends CommandRunner {
12
+ constructor() {
13
+ super();
14
+ }
15
+
16
+ run(): Promise<void> {
17
+ this.command.help();
18
+ return Promise.resolve();
19
+ }
20
+ }
@@ -1,6 +1,5 @@
1
- import { Option } from 'nest-commander';
1
+ import { CommandRunner, Option } from 'nest-commander';
2
2
  import { ConfigService } from '@nestjs/config';
3
- import { BaseCommand } from 'src/commands/base.command';
4
3
  import { SyncApiService, CommitRevisionService } from 'src/services/sync';
5
4
  import {
6
5
  RevisiumUrlComplete,
@@ -21,7 +20,7 @@ export interface DataSyncOptions extends BaseSyncOptions {
21
20
  batchSize?: number;
22
21
  }
23
22
 
24
- export abstract class BaseSyncCommand extends BaseCommand {
23
+ export abstract class BaseSyncCommand extends CommandRunner {
25
24
  constructor(
26
25
  protected readonly configService: ConfigService,
27
26
  protected readonly urlBuilder: UrlBuilderService,
@@ -2,6 +2,7 @@ import { CommandRunner, Option } from 'nest-commander';
2
2
 
3
3
  export type BaseOptions = {
4
4
  url?: string;
5
+ context?: string;
5
6
  };
6
7
 
7
8
  export abstract class BaseCommand extends CommandRunner {
@@ -14,4 +15,14 @@ export abstract class BaseCommand extends CommandRunner {
14
15
  public parseUrl(value: string) {
15
16
  return value;
16
17
  }
18
+
19
+ @Option({
20
+ flags: '--context <name>',
21
+ description:
22
+ 'Named workspace context from .revisium/revisium-cli.config.json (single-target commands)',
23
+ required: false,
24
+ })
25
+ public parseContext(value: string) {
26
+ return value;
27
+ }
17
28
  }
@@ -0,0 +1,173 @@
1
+ import { CommandRunner, Option, SubCommand } from 'nest-commander';
2
+ import { LoggerService } from 'src/services/common';
3
+ import {
4
+ DEFAULT_BRANCH,
5
+ DEFAULT_REVISION,
6
+ WorkspaceConfigService,
7
+ WorkspaceConfig,
8
+ WorkspaceContextConfig,
9
+ } from 'src/services/workspace';
10
+
11
+ interface Options {
12
+ url?: string;
13
+ instance?: string;
14
+ credential?: string;
15
+ org?: string;
16
+ project?: string;
17
+ branch?: string;
18
+ revision?: string;
19
+ }
20
+
21
+ @SubCommand({
22
+ name: 'create',
23
+ arguments: '<name>',
24
+ description: 'Create or update a workspace Revisium context',
25
+ })
26
+ export class ContextCreateCommand extends CommandRunner {
27
+ constructor(
28
+ private readonly workspaceConfig: WorkspaceConfigService,
29
+ private readonly logger: LoggerService,
30
+ ) {
31
+ super();
32
+ }
33
+
34
+ async run(inputs: string[], options: Options): Promise<void> {
35
+ const name = inputs[0];
36
+ if (!name) {
37
+ throw new Error('Error: context name is required');
38
+ }
39
+
40
+ const loaded = await this.workspaceConfig.loadOrCreate();
41
+ const context = this.buildContext(loaded.config, options);
42
+ const existed = Boolean(loaded.config.contexts[name]);
43
+
44
+ loaded.config.contexts[name] = context;
45
+ if (!loaded.config.currentContext) {
46
+ loaded.config.currentContext = name;
47
+ }
48
+
49
+ await this.workspaceConfig.save(loaded.path, loaded.config);
50
+
51
+ this.logger.success(
52
+ `${existed ? 'Updated' : 'Created'} context "${name}" (${context.organization}/${context.project}/${context.branch || DEFAULT_BRANCH}:${context.revision || DEFAULT_REVISION})`,
53
+ );
54
+ this.logger.info(`Instance: ${context.instance}`);
55
+ this.logger.info(`Config: ${loaded.path}`);
56
+ }
57
+
58
+ private buildContext(
59
+ config: WorkspaceConfig,
60
+ options: Options,
61
+ ): WorkspaceContextConfig {
62
+ if (options.url) {
63
+ const parsed = this.workspaceConfig.parseContextUrl(options.url);
64
+ const instanceName =
65
+ options.instance ||
66
+ this.workspaceConfig.findInstanceNameByBaseUrl(config, parsed.baseUrl);
67
+
68
+ if (!instanceName || !config.instances[instanceName]) {
69
+ throw new Error(
70
+ `No instance found for ${parsed.baseUrl}. Run: revisium instance add <name> --url ${options.url}`,
71
+ );
72
+ }
73
+
74
+ return this.withCredential(
75
+ {
76
+ instance: instanceName,
77
+ organization: parsed.organization,
78
+ project: parsed.project,
79
+ branch: parsed.branch,
80
+ revision: parsed.revision,
81
+ },
82
+ options.credential,
83
+ );
84
+ }
85
+
86
+ if (!options.instance || !options.org || !options.project) {
87
+ throw new Error(
88
+ 'Error: provide --url, or provide --instance, --org, and --project',
89
+ );
90
+ }
91
+
92
+ if (!config.instances[options.instance]) {
93
+ throw new Error(`Revisium instance "${options.instance}" was not found`);
94
+ }
95
+
96
+ return this.withCredential(
97
+ {
98
+ instance: options.instance,
99
+ organization: options.org,
100
+ project: options.project,
101
+ branch: options.branch || DEFAULT_BRANCH,
102
+ revision: options.revision || DEFAULT_REVISION,
103
+ },
104
+ options.credential,
105
+ );
106
+ }
107
+
108
+ private withCredential(
109
+ context: WorkspaceContextConfig,
110
+ credential: string | undefined,
111
+ ): WorkspaceContextConfig {
112
+ if (credential) {
113
+ return { ...context, credential };
114
+ }
115
+ return context;
116
+ }
117
+
118
+ @Option({
119
+ flags: '--url <url>',
120
+ description: 'Revisium target URL: revisium://host/org/project/branch',
121
+ })
122
+ parseUrl(value: string) {
123
+ return value;
124
+ }
125
+
126
+ @Option({
127
+ flags: '--instance <name>',
128
+ description: 'Instance name from workspace config',
129
+ })
130
+ parseInstance(value: string) {
131
+ return value;
132
+ }
133
+
134
+ @Option({
135
+ flags: '--credential <name>',
136
+ description: 'Named saved credential selector for stored auth',
137
+ })
138
+ parseCredential(value: string) {
139
+ return value;
140
+ }
141
+
142
+ @Option({
143
+ flags: '--org <organization>',
144
+ description: 'Organization id',
145
+ })
146
+ parseOrg(value: string) {
147
+ return value;
148
+ }
149
+
150
+ @Option({
151
+ flags: '--project <project>',
152
+ description: 'Project name',
153
+ })
154
+ parseProject(value: string) {
155
+ return value;
156
+ }
157
+
158
+ @Option({
159
+ flags: '--branch <branch>',
160
+ description: 'Branch name (default: master)',
161
+ })
162
+ parseBranch(value: string) {
163
+ return value;
164
+ }
165
+
166
+ @Option({
167
+ flags: '--revision <revision>',
168
+ description: 'Revision target (default: draft)',
169
+ })
170
+ parseRevision(value: string) {
171
+ return value;
172
+ }
173
+ }
@@ -0,0 +1,36 @@
1
+ import { CommandRunner, SubCommand } from 'nest-commander';
2
+ import { LoggerService } from 'src/services/common';
3
+ import {
4
+ DEFAULT_BRANCH,
5
+ DEFAULT_REVISION,
6
+ WorkspaceConfigService,
7
+ } from 'src/services/workspace';
8
+
9
+ @SubCommand({
10
+ name: 'list',
11
+ description: 'List workspace Revisium contexts',
12
+ })
13
+ export class ContextListCommand extends CommandRunner {
14
+ constructor(
15
+ private readonly workspaceConfig: WorkspaceConfigService,
16
+ private readonly logger: LoggerService,
17
+ ) {
18
+ super();
19
+ }
20
+
21
+ async run(): Promise<void> {
22
+ const loaded = await this.workspaceConfig.load();
23
+ if (!loaded || Object.keys(loaded.config.contexts).length === 0) {
24
+ this.logger.info('No Revisium contexts configured in this workspace.');
25
+ return;
26
+ }
27
+
28
+ this.logger.info(`Config: ${loaded.path}`);
29
+ for (const [name, context] of Object.entries(loaded.config.contexts)) {
30
+ const marker = loaded.config.currentContext === name ? '*' : ' ';
31
+ this.logger.info(
32
+ `${marker} ${name}\t${context.instance}\t${context.organization}/${context.project}/${context.branch || DEFAULT_BRANCH}:${context.revision || DEFAULT_REVISION}`,
33
+ );
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,35 @@
1
+ import { CommandRunner, SubCommand } from 'nest-commander';
2
+ import { LoggerService } from 'src/services/common';
3
+ import { WorkspaceConfigService } from 'src/services/workspace';
4
+
5
+ @SubCommand({
6
+ name: 'remove',
7
+ arguments: '<name>',
8
+ description: 'Remove a workspace Revisium context',
9
+ })
10
+ export class ContextRemoveCommand extends CommandRunner {
11
+ constructor(
12
+ private readonly workspaceConfig: WorkspaceConfigService,
13
+ private readonly logger: LoggerService,
14
+ ) {
15
+ super();
16
+ }
17
+
18
+ async run(inputs: string[]): Promise<void> {
19
+ const name = inputs[0];
20
+ if (!name) {
21
+ throw new Error('Error: context name is required');
22
+ }
23
+
24
+ const { loaded } = await this.workspaceConfig.loadContext(name);
25
+
26
+ delete loaded.config.contexts[name];
27
+ if (loaded.config.currentContext === name) {
28
+ delete loaded.config.currentContext;
29
+ }
30
+
31
+ await this.workspaceConfig.save(loaded.path, loaded.config);
32
+
33
+ this.logger.success(`Removed context "${name}"`);
34
+ }
35
+ }
@@ -0,0 +1,59 @@
1
+ import { CommandRunner, SubCommand } from 'nest-commander';
2
+ import { LoggerService } from 'src/services/common';
3
+ import {
4
+ DEFAULT_BRANCH,
5
+ DEFAULT_CREDENTIAL,
6
+ DEFAULT_REVISION,
7
+ WorkspaceConfigService,
8
+ } from 'src/services/workspace';
9
+
10
+ @SubCommand({
11
+ name: 'show',
12
+ arguments: '[name]',
13
+ description: 'Show a workspace Revisium context',
14
+ })
15
+ export class ContextShowCommand extends CommandRunner {
16
+ constructor(
17
+ private readonly workspaceConfig: WorkspaceConfigService,
18
+ private readonly logger: LoggerService,
19
+ ) {
20
+ super();
21
+ }
22
+
23
+ async run(inputs: string[]): Promise<void> {
24
+ const loaded = await this.workspaceConfig.load();
25
+ if (!loaded) {
26
+ throw new Error('No Revisium workspace config found');
27
+ }
28
+
29
+ const name = inputs[0] || loaded.config.currentContext;
30
+ if (!name) {
31
+ throw new Error('No current Revisium context is configured');
32
+ }
33
+
34
+ const context = loaded.config.contexts[name];
35
+ if (!context) {
36
+ throw new Error(`Revisium context "${name}" was not found`);
37
+ }
38
+
39
+ const instance = loaded.config.instances[context.instance];
40
+
41
+ this.logger.info(`Name: ${name}`);
42
+ this.logger.info(`Instance: ${context.instance}`);
43
+ if (instance) {
44
+ this.logger.info(`Base URL: ${instance.baseUrl}`);
45
+ this.logger.info(`Auth mode: ${instance.authMode || 'stored'}`);
46
+ } else {
47
+ this.logger.warn(`Instance "${context.instance}" not found in config`);
48
+ }
49
+ this.logger.info(
50
+ `Target: ${context.organization}/${context.project}/${context.branch || DEFAULT_BRANCH}:${context.revision || DEFAULT_REVISION}`,
51
+ );
52
+ if ((instance?.authMode || 'stored') === 'stored') {
53
+ this.logger.info(
54
+ `Credential: ${context.credential || DEFAULT_CREDENTIAL}`,
55
+ );
56
+ }
57
+ this.logger.info(`Config: ${loaded.path}`);
58
+ }
59
+ }
@@ -0,0 +1,31 @@
1
+ import { CommandRunner, SubCommand } from 'nest-commander';
2
+ import { LoggerService } from 'src/services/common';
3
+ import { WorkspaceConfigService } from 'src/services/workspace';
4
+
5
+ @SubCommand({
6
+ name: 'use',
7
+ arguments: '<name>',
8
+ description: 'Set the current workspace Revisium context',
9
+ })
10
+ export class ContextUseCommand extends CommandRunner {
11
+ constructor(
12
+ private readonly workspaceConfig: WorkspaceConfigService,
13
+ private readonly logger: LoggerService,
14
+ ) {
15
+ super();
16
+ }
17
+
18
+ async run(inputs: string[]): Promise<void> {
19
+ const name = inputs[0];
20
+ if (!name) {
21
+ throw new Error('Error: context name is required');
22
+ }
23
+
24
+ const { loaded } = await this.workspaceConfig.loadContext(name);
25
+
26
+ loaded.config.currentContext = name;
27
+ await this.workspaceConfig.save(loaded.path, loaded.config);
28
+
29
+ this.logger.success(`Using context "${name}"`);
30
+ }
31
+ }
@@ -0,0 +1,28 @@
1
+ import { Command, CommandRunner } from 'nest-commander';
2
+ import { ContextCreateCommand } from './context-create.command';
3
+ import { ContextListCommand } from './context-list.command';
4
+ import { ContextRemoveCommand } from './context-remove.command';
5
+ import { ContextShowCommand } from './context-show.command';
6
+ import { ContextUseCommand } from './context-use.command';
7
+
8
+ @Command({
9
+ name: 'context',
10
+ description: 'Manage workspace Revisium contexts',
11
+ subCommands: [
12
+ ContextCreateCommand,
13
+ ContextListCommand,
14
+ ContextShowCommand,
15
+ ContextUseCommand,
16
+ ContextRemoveCommand,
17
+ ],
18
+ })
19
+ export class ContextCommand extends CommandRunner {
20
+ constructor() {
21
+ super();
22
+ }
23
+
24
+ run(): Promise<void> {
25
+ this.command.help();
26
+ return Promise.resolve();
27
+ }
28
+ }
@@ -0,0 +1,129 @@
1
+ import { EndpointEnsureCommand } from '../endpoint-ensure.command';
2
+ import { BootstrapService } from 'src/services/bootstrap';
3
+ import { LoggerService } from 'src/services/common';
4
+
5
+ describe('EndpointEnsureCommand', () => {
6
+ let bootstrapService: {
7
+ ensureEndpoint: jest.Mock;
8
+ resolveTarget: jest.Mock;
9
+ formatEndpointHint: jest.Mock;
10
+ parseEndpointType: jest.Mock;
11
+ };
12
+ let logger: { info: jest.Mock; success: jest.Mock };
13
+ let command: EndpointEnsureCommand;
14
+ let logSpy: jest.SpyInstance;
15
+
16
+ beforeEach(() => {
17
+ bootstrapService = {
18
+ ensureEndpoint: jest.fn().mockResolvedValue({
19
+ endpoint: { id: 'rest-id', type: 'REST_API' },
20
+ status: 'created',
21
+ revision: 'draft',
22
+ dryRun: false,
23
+ }),
24
+ resolveTarget: jest.fn().mockResolvedValue({
25
+ baseUrl: 'http://localhost:9222',
26
+ organization: 'admin',
27
+ project: 'dictionary',
28
+ branch: 'master',
29
+ revision: 'draft',
30
+ }),
31
+ formatEndpointHint: jest
32
+ .fn()
33
+ .mockReturnValue('http://localhost:9222/endpoint/rest/admin/x/y/draft'),
34
+ parseEndpointType: jest.fn().mockReturnValue('REST_API'),
35
+ };
36
+ logger = { info: jest.fn(), success: jest.fn() };
37
+ command = new EndpointEnsureCommand(
38
+ bootstrapService as unknown as BootstrapService,
39
+ logger as unknown as LoggerService,
40
+ );
41
+ logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
42
+ });
43
+
44
+ afterEach(() => {
45
+ logSpy.mockRestore();
46
+ });
47
+
48
+ it('rejects when --type is missing', async () => {
49
+ await expect(command.run([], { type: undefined as never })).rejects.toThrow(
50
+ '--type option is required',
51
+ );
52
+ });
53
+
54
+ it('logs created endpoint info with hint', async () => {
55
+ await command.run([], { type: 'REST_API' });
56
+
57
+ expect(bootstrapService.ensureEndpoint).toHaveBeenCalledWith(
58
+ { type: 'REST_API' },
59
+ 'REST_API',
60
+ undefined,
61
+ );
62
+ expect(logger.success).toHaveBeenCalledWith(
63
+ 'Created endpoint rest-id (REST_API)',
64
+ );
65
+ expect(logger.info).toHaveBeenCalledWith('Revision: draft');
66
+ expect(logger.info).toHaveBeenCalledWith(
67
+ 'Hint: http://localhost:9222/endpoint/rest/admin/x/y/draft',
68
+ );
69
+ });
70
+
71
+ it('shows "Found" when endpoint already exists', async () => {
72
+ bootstrapService.ensureEndpoint.mockResolvedValue({
73
+ endpoint: { id: 'rest-id', type: 'REST_API' },
74
+ status: 'skipped',
75
+ revision: 'draft',
76
+ dryRun: false,
77
+ });
78
+
79
+ await command.run([], { type: 'REST_API' });
80
+
81
+ expect(logger.success).toHaveBeenCalledWith(
82
+ 'Found endpoint rest-id (REST_API)',
83
+ );
84
+ });
85
+
86
+ it('uses <dry-run> placeholder when no endpoint id is returned', async () => {
87
+ bootstrapService.ensureEndpoint.mockResolvedValue({
88
+ endpoint: { type: 'GRAPHQL' },
89
+ status: 'created',
90
+ revision: 'draft',
91
+ dryRun: true,
92
+ });
93
+
94
+ await command.run([], { type: 'GRAPHQL', dryRun: true });
95
+
96
+ expect(logger.success).toHaveBeenCalledWith(
97
+ 'Created endpoint <dry-run> (GRAPHQL)',
98
+ );
99
+ });
100
+
101
+ it('emits JSON when --json is set', async () => {
102
+ await command.run([], { type: 'REST_API', json: true });
103
+
104
+ expect(logSpy).toHaveBeenCalledTimes(1);
105
+ const firstCall = logSpy.mock.calls[0] as unknown[];
106
+ const payload = JSON.parse(firstCall[0] as string) as Record<
107
+ string,
108
+ unknown
109
+ >;
110
+ expect(payload).toEqual({
111
+ endpoint: { id: 'rest-id', type: 'REST_API' },
112
+ status: 'created',
113
+ revision: 'draft',
114
+ dryRun: false,
115
+ hint: 'http://localhost:9222/endpoint/rest/admin/x/y/draft',
116
+ });
117
+ expect(logger.success).not.toHaveBeenCalled();
118
+ });
119
+
120
+ it('delegates option parsing to BootstrapService.parseEndpointType', () => {
121
+ expect(command.parseType('REST_API')).toBe('REST_API');
122
+ expect(bootstrapService.parseEndpointType).toHaveBeenCalledWith('REST_API');
123
+ });
124
+
125
+ it('parses boolean flags', () => {
126
+ expect(command.parseDryRun('true')).toBe(true);
127
+ expect(command.parseJson()).toBe(true);
128
+ });
129
+ });