From ea6a248b1e0ea959f9ad3ab2dbcd6464b27439a7 Mon Sep 17 00:00:00 2001 From: c19yamahar Date: Sat, 17 Oct 2026 20:58:40 +0900 Subject: [PATCH 1/8] feat(version-manager): add side-effect-free version resolution --- apps/generator-cli/src/README.md | 4 +- apps/generator-cli/src/app/app.module.spec.ts | 31 ++++++++++++++ apps/generator-cli/src/app/app.module.ts | 17 +++++++- .../version-manager.controller.spec.ts | 42 +++++++++++++++++-- .../controllers/version-manager.controller.ts | 20 +++++++++ .../src/app/services/config.service.spec.ts | 8 ++-- .../src/app/services/config.service.ts | 3 +- 7 files changed, 111 insertions(+), 14 deletions(-) diff --git a/apps/generator-cli/src/README.md b/apps/generator-cli/src/README.md index 95cf9b14162..da25612b08f 100644 --- a/apps/generator-cli/src/README.md +++ b/apps/generator-cli/src/README.md @@ -38,6 +38,9 @@ you could change it by using one of the following commands: - `openapi-generator-cli version-manager list` - `openapi-generator-cli version-manager set ` +- `openapi-generator-cli version-manager resolve ` + +The `resolve` command prints the matching version without downloading a generator or modifying `openapitools.json`. #### [added] generator config @@ -308,4 +311,3 @@ yarn add @openapitools/openapi-generator-cli@1.0.18-4.3.1 Please leave a star. - diff --git a/apps/generator-cli/src/app/app.module.spec.ts b/apps/generator-cli/src/app/app.module.spec.ts index 3a7224deebb..76886347536 100644 --- a/apps/generator-cli/src/app/app.module.spec.ts +++ b/apps/generator-cli/src/app/app.module.spec.ts @@ -115,6 +115,37 @@ describe('AppModule', () => { }); }); + describe('version-manager resolve is provided', () => { + beforeEach(async () => { + process.argv = ['foo', 'baz', 'version-manager', 'resolve', 'latest']; + programMock.parse.mockImplementation(() => { + expect(passThroughServiceMock.init).toHaveBeenCalledTimes(0); + }); + versionManagerServiceMock.getSelectedVersion.mockReturnValue('1.2.3'); + await fixture.onApplicationBootstrap(); + }); + + it('does not search for the latest version', () => { + expect(versionManagerServiceMock.search).toHaveBeenCalledTimes(0); + }); + + it('does not set the selected version', () => { + expect( + versionManagerServiceMock.setSelectedVersion, + ).toHaveBeenCalledTimes(0); + }); + + it('does not download', () => { + expect( + versionManagerServiceMock.downloadIfNeeded, + ).toHaveBeenCalledTimes(0); + }); + + it('parses the command', () => { + expect(programMock.parse).toHaveBeenNthCalledWith(1, process.argv); + }); + }); + describe('a custom generator is provided', () => { beforeEach(async () => { process.argv = [ diff --git a/apps/generator-cli/src/app/app.module.ts b/apps/generator-cli/src/app/app.module.ts index 50787621e92..8435c1979ce 100644 --- a/apps/generator-cli/src/app/app.module.ts +++ b/apps/generator-cli/src/app/app.module.ts @@ -62,12 +62,14 @@ export class AppModule implements OnApplicationBootstrap { ) {} onApplicationBootstrap = async () => { + const isVersionManagerResolveCommand = + this.isVersionManagerResolveCommand(); const hasCustomGenerator = process.argv.some( (arg) => arg === '--custom-generator' || arg.startsWith('--custom-generator='), ); - if (!hasCustomGenerator) { + if (!hasCustomGenerator && !isVersionManagerResolveCommand) { let selectedVersion = this.versionManager.getSelectedVersion(); if (!selectedVersion) { @@ -81,7 +83,18 @@ export class AppModule implements OnApplicationBootstrap { await this.versionManager.downloadIfNeeded(selectedVersion); } - await this.passThroughService.init(); + if (!isVersionManagerResolveCommand) { + await this.passThroughService.init(); + } this.program.parse(process.argv); }; + + private isVersionManagerResolveCommand() { + const args = process.argv.slice(2); + + return args.some( + (arg, index) => + arg === 'version-manager' && args[index + 1] === 'resolve', + ); + } } diff --git a/apps/generator-cli/src/app/controllers/version-manager.controller.spec.ts b/apps/generator-cli/src/app/controllers/version-manager.controller.spec.ts index 9a3133c6840..9e49d7a0760 100644 --- a/apps/generator-cli/src/app/controllers/version-manager.controller.spec.ts +++ b/apps/generator-cli/src/app/controllers/version-manager.controller.spec.ts @@ -47,10 +47,10 @@ describe('VersionManagerController', () => { }).compile(); }); - it('adds 3 commands, but 2 actions', () => { - expect(commandMock.action).toHaveBeenCalledTimes(2); - expect(commandMock.command).toHaveBeenCalledTimes(3); - expect(commandMock.description).toHaveBeenCalledTimes(3); + it('adds 4 commands, but 3 actions', () => { + expect(commandMock.action).toHaveBeenCalledTimes(3); + expect(commandMock.command).toHaveBeenCalledTimes(4); + expect(commandMock.description).toHaveBeenCalledTimes(4); }); describe('commands', function () { @@ -310,5 +310,39 @@ describe('VersionManagerController', () => { ); }); }); + + describe('resolve', () => { + beforeEach(() => { + cmd = commandMock.commands['resolve ']; + }); + + it('has the correct description', () => { + expect(cmd.description).toEqual('resolve version tags to a version'); + }); + + it('prints the first matching version', async () => { + versionManagerServiceMock.search.mockReturnValue( + of([{ version: '1.2.3' }, { version: '1.2.4' }]), + ); + + await cmd.action(['latest']); + + expect(versionManagerServiceMock.search).toHaveBeenNthCalledWith(1, [ + 'latest', + ]); + expect(log).toHaveBeenNthCalledWith(1, '1.2.3'); + }); + + it('prints a message, if the search result is empty', async () => { + versionManagerServiceMock.search.mockReturnValue(of([])); + + await cmd.action(['latest']); + + expect(log).toHaveBeenNthCalledWith( + 1, + chalk.red('Unable to find version matching criteria "latest"'), + ); + }); + }); }); }); diff --git a/apps/generator-cli/src/app/controllers/version-manager.controller.ts b/apps/generator-cli/src/app/controllers/version-manager.controller.ts index e5c652600b5..af464f183fb 100644 --- a/apps/generator-cli/src/app/controllers/version-manager.controller.ts +++ b/apps/generator-cli/src/app/controllers/version-manager.controller.ts @@ -22,6 +22,11 @@ export class VersionManagerController { .description('set version to use') .action((tags) => this.set(tags)); + private readonly resolveCommand = this.mainCommand + .command('resolve ') + .description('resolve version tags to a version') + .action((tags) => this.resolve(tags)); + constructor( @Inject(LOGGER) private readonly logger: LOGGER, @Inject(COMMANDER_PROGRAM) private readonly program: Command, @@ -95,6 +100,21 @@ export class VersionManagerController { ); }; + private resolve = async (versionTags: string[]) => { + const versions = await this.service.search(versionTags).toPromise(); + + if (versions.length > 0) { + this.logger.log(versions[0].version); + return; + } + + this.logger.log( + chalk.red( + `Unable to find version matching criteria "${versionTags.join(' ')}"`, + ), + ); + }; + private table = (versions: Version[]) => { // Keep row order identical to the order returned in versions. const rows = versions.map((version) => { diff --git a/apps/generator-cli/src/app/services/config.service.spec.ts b/apps/generator-cli/src/app/services/config.service.spec.ts index cc37a0a4461..2febd85b27b 100644 --- a/apps/generator-cli/src/app/services/config.service.spec.ts +++ b/apps/generator-cli/src/app/services/config.service.spec.ts @@ -69,12 +69,10 @@ describe('ConfigService', () => { }); }); - it('ensures the config file', () => { + it('does not create or modify the config file', () => { fixture.get('some-path'); - expect(fs.ensureFileSync).toHaveBeenNthCalledWith( - 1, - fixture.configFile - ); + expect(fs.ensureFileSync).toHaveBeenCalledTimes(0); + expect(fs.writeJSONSync).toHaveBeenCalledTimes(0); }); it.each([ diff --git a/apps/generator-cli/src/app/services/config.service.ts b/apps/generator-cli/src/app/services/config.service.ts index e087e00d919..e7a68f345c4 100644 --- a/apps/generator-cli/src/app/services/config.service.ts +++ b/apps/generator-cli/src/app/services/config.service.ts @@ -141,8 +141,6 @@ export class ConfigService { return result; }; - fs.ensureFileSync(this.configFile); - const fileConfig = fs.readJSONSync(this.configFile, { throws: false, encoding: 'utf8' }) ?? {}; @@ -196,6 +194,7 @@ export class ConfigService { } private write(config) { + fs.ensureFileSync(this.configFile); fs.writeJSONSync(this.configFile, config, {encoding: 'utf8', spaces: config.spaces || 2}) } } From e62aa94cbfc11d289828debd29a7dd843a613506 Mon Sep 17 00:00:00 2001 From: c19yamahar Date: Sat, 17 Oct 2026 21:11:12 +0900 Subject: [PATCH 2/8] fix(version-manager): fail resolution on missing tags --- .../src/app/controllers/version-manager.controller.spec.ts | 7 ++++++- .../src/app/controllers/version-manager.controller.ts | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/generator-cli/src/app/controllers/version-manager.controller.spec.ts b/apps/generator-cli/src/app/controllers/version-manager.controller.spec.ts index 9e49d7a0760..06db9d841d6 100644 --- a/apps/generator-cli/src/app/controllers/version-manager.controller.spec.ts +++ b/apps/generator-cli/src/app/controllers/version-manager.controller.spec.ts @@ -316,6 +316,10 @@ describe('VersionManagerController', () => { cmd = commandMock.commands['resolve ']; }); + afterEach(() => { + process.exitCode = 0; + }); + it('has the correct description', () => { expect(cmd.description).toEqual('resolve version tags to a version'); }); @@ -333,7 +337,7 @@ describe('VersionManagerController', () => { expect(log).toHaveBeenNthCalledWith(1, '1.2.3'); }); - it('prints a message, if the search result is empty', async () => { + it('prints a message and sets a non-zero exit code if the search result is empty', async () => { versionManagerServiceMock.search.mockReturnValue(of([])); await cmd.action(['latest']); @@ -342,6 +346,7 @@ describe('VersionManagerController', () => { 1, chalk.red('Unable to find version matching criteria "latest"'), ); + expect(process.exitCode).toBe(1); }); }); }); diff --git a/apps/generator-cli/src/app/controllers/version-manager.controller.ts b/apps/generator-cli/src/app/controllers/version-manager.controller.ts index af464f183fb..39cf8564d68 100644 --- a/apps/generator-cli/src/app/controllers/version-manager.controller.ts +++ b/apps/generator-cli/src/app/controllers/version-manager.controller.ts @@ -113,6 +113,7 @@ export class VersionManagerController { `Unable to find version matching criteria "${versionTags.join(' ')}"`, ), ); + process.exitCode = 1; }; private table = (versions: Version[]) => { From 1a978e5066ae94b88eb57dd4ba249ade7de13b43 Mon Sep 17 00:00:00 2001 From: c19yamahar Date: Sun, 18 Oct 2026 01:57:42 +0900 Subject: [PATCH 3/8] docs(version-manager): clarify resolve behavior --- apps/generator-cli/src/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/generator-cli/src/README.md b/apps/generator-cli/src/README.md index da25612b08f..3486db1f6cf 100644 --- a/apps/generator-cli/src/README.md +++ b/apps/generator-cli/src/README.md @@ -34,12 +34,13 @@ That version is saved in the file *openapitools.json*. Therefore, you should inc to ensure that the correct version is being used next time you call the command. If you would like to use a different version of the [OpenAPITools/openapi-generator](https://github.com/OpenAPITools/openapi-generator), -you could change it by using one of the following commands: +you can use one of the following commands: - `openapi-generator-cli version-manager list` - `openapi-generator-cli version-manager set ` - `openapi-generator-cli version-manager resolve ` +The `list` and `set` commands can be used to inspect or change the configured version. The `resolve` command prints the matching version without downloading a generator or modifying `openapitools.json`. #### [added] generator config @@ -310,4 +311,3 @@ yarn add @openapitools/openapi-generator-cli@1.0.18-4.3.1 Please leave a star. - From d7f0e8f85c59a8f913645cdf3750070513e12bf2 Mon Sep 17 00:00:00 2001 From: c19yamahar Date: Sun, 18 Oct 2026 01:57:53 +0900 Subject: [PATCH 4/8] test(config): cover missing config read behavior --- apps/generator-cli/src/app/services/config.service.spec.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/generator-cli/src/app/services/config.service.spec.ts b/apps/generator-cli/src/app/services/config.service.spec.ts index 2febd85b27b..9947c2f4f72 100644 --- a/apps/generator-cli/src/app/services/config.service.spec.ts +++ b/apps/generator-cli/src/app/services/config.service.spec.ts @@ -56,6 +56,12 @@ describe('ConfigService', () => { it('can return a default value', () => { expect(fixture.get('unknown', 'foo')).toEqual('foo'); }); + + it('does not create or modify the config file', () => { + fixture.get('some-path'); + expect(fs.ensureFileSync).toHaveBeenCalledTimes(0); + expect(fs.writeJSONSync).toHaveBeenCalledTimes(0); + }); }); describe('the config has values', () => { From d7ee4e898a936910b592774d6e02bc0426d5c5d7 Mon Sep 17 00:00:00 2001 From: c19yamahar Date: Sun, 18 Oct 2026 01:58:02 +0900 Subject: [PATCH 5/8] test(version-manager): assert highest resolved version --- .../src/app/controllers/version-manager.controller.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/generator-cli/src/app/controllers/version-manager.controller.spec.ts b/apps/generator-cli/src/app/controllers/version-manager.controller.spec.ts index 06db9d841d6..1c54877d550 100644 --- a/apps/generator-cli/src/app/controllers/version-manager.controller.spec.ts +++ b/apps/generator-cli/src/app/controllers/version-manager.controller.spec.ts @@ -326,7 +326,7 @@ describe('VersionManagerController', () => { it('prints the first matching version', async () => { versionManagerServiceMock.search.mockReturnValue( - of([{ version: '1.2.3' }, { version: '1.2.4' }]), + of([{ version: '1.2.4' }, { version: '1.2.3' }]), ); await cmd.action(['latest']); @@ -334,7 +334,7 @@ describe('VersionManagerController', () => { expect(versionManagerServiceMock.search).toHaveBeenNthCalledWith(1, [ 'latest', ]); - expect(log).toHaveBeenNthCalledWith(1, '1.2.3'); + expect(log).toHaveBeenNthCalledWith(1, '1.2.4'); }); it('prints a message and sets a non-zero exit code if the search result is empty', async () => { From cbe24211ec94db3c2274cb8aef82d5a3628a4074 Mon Sep 17 00:00:00 2001 From: c19yamahar Date: Sun, 18 Oct 2026 01:58:25 +0900 Subject: [PATCH 6/8] fix(version-manager): write resolve errors to stderr --- .../src/app/controllers/version-manager.controller.spec.ts | 6 ++++-- .../src/app/controllers/version-manager.controller.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/generator-cli/src/app/controllers/version-manager.controller.spec.ts b/apps/generator-cli/src/app/controllers/version-manager.controller.spec.ts index 1c54877d550..5fe16a10f93 100644 --- a/apps/generator-cli/src/app/controllers/version-manager.controller.spec.ts +++ b/apps/generator-cli/src/app/controllers/version-manager.controller.spec.ts @@ -12,6 +12,7 @@ describe('VersionManagerController', () => { let commandMock: CommandMock; const log = jest.fn(); + const error = jest.fn(); const uiServiceMock = { table: jest.fn(), @@ -32,6 +33,7 @@ describe('VersionManagerController', () => { [ log, + error, ...Object.values(uiServiceMock), ...Object.values(versionManagerServiceMock), ].forEach((spy) => spy.mockReset()); @@ -42,7 +44,7 @@ describe('VersionManagerController', () => { { provide: UIService, useValue: uiServiceMock }, { provide: VersionManagerService, useValue: versionManagerServiceMock }, { provide: COMMANDER_PROGRAM, useValue: commandMock }, - { provide: LOGGER, useValue: { log } }, + { provide: LOGGER, useValue: { log, error } }, ], }).compile(); }); @@ -342,7 +344,7 @@ describe('VersionManagerController', () => { await cmd.action(['latest']); - expect(log).toHaveBeenNthCalledWith( + expect(error).toHaveBeenNthCalledWith( 1, chalk.red('Unable to find version matching criteria "latest"'), ); diff --git a/apps/generator-cli/src/app/controllers/version-manager.controller.ts b/apps/generator-cli/src/app/controllers/version-manager.controller.ts index 39cf8564d68..4137471cf22 100644 --- a/apps/generator-cli/src/app/controllers/version-manager.controller.ts +++ b/apps/generator-cli/src/app/controllers/version-manager.controller.ts @@ -108,7 +108,7 @@ export class VersionManagerController { return; } - this.logger.log( + this.logger.error( chalk.red( `Unable to find version matching criteria "${versionTags.join(' ')}"`, ), From 8353fa03ebe9f2e898564f36b3982ed3d53b5a8d Mon Sep 17 00:00:00 2001 From: c19yamahar Date: Sun, 18 Oct 2026 01:58:52 +0900 Subject: [PATCH 7/8] fix(cli): detect resolver command positionally --- apps/generator-cli/src/app/app.module.spec.ts | 19 +++++++++++++++ apps/generator-cli/src/app/app.module.ts | 23 ++++++++++++++++--- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/apps/generator-cli/src/app/app.module.spec.ts b/apps/generator-cli/src/app/app.module.spec.ts index 76886347536..767c54b4260 100644 --- a/apps/generator-cli/src/app/app.module.spec.ts +++ b/apps/generator-cli/src/app/app.module.spec.ts @@ -146,6 +146,25 @@ describe('AppModule', () => { }); }); + describe('a pass-through command contains version-manager resolve as arguments', () => { + beforeEach(async () => { + process.argv = [ + 'foo', + 'baz', + 'generate', + '--some-option', + 'version-manager', + 'resolve', + ]; + versionManagerServiceMock.getSelectedVersion.mockReturnValue('1.2.3'); + await fixture.onApplicationBootstrap(); + }); + + it('initializes the pass-through service', () => { + expect(passThroughServiceMock.init).toHaveBeenCalledTimes(1); + }); + }); + describe('a custom generator is provided', () => { beforeEach(async () => { process.argv = [ diff --git a/apps/generator-cli/src/app/app.module.ts b/apps/generator-cli/src/app/app.module.ts index 8435c1979ce..3bdd5ee292e 100644 --- a/apps/generator-cli/src/app/app.module.ts +++ b/apps/generator-cli/src/app/app.module.ts @@ -91,10 +91,27 @@ export class AppModule implements OnApplicationBootstrap { private isVersionManagerResolveCommand() { const args = process.argv.slice(2); + let commandIndex = 0; - return args.some( - (arg, index) => - arg === 'version-manager' && args[index + 1] === 'resolve', + while (commandIndex < args.length) { + const arg = args[commandIndex]; + + if (arg === '--openapitools') { + commandIndex += 2; + continue; + } + + if (arg.startsWith('--openapitools=')) { + commandIndex += 1; + continue; + } + + break; + } + + return ( + args[commandIndex] === 'version-manager' && + args[commandIndex + 1] === 'resolve' ); } } From 305bb5483b0bb5674ccdcabfafd297f6ec9c7dc0 Mon Sep 17 00:00:00 2001 From: c19yamahar Date: Sun, 18 Oct 2026 02:05:14 +0900 Subject: [PATCH 8/8] test(cli): cover resolver config option forms --- apps/generator-cli/src/app/app.module.spec.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/apps/generator-cli/src/app/app.module.spec.ts b/apps/generator-cli/src/app/app.module.spec.ts index 767c54b4260..df3a81b6cdd 100644 --- a/apps/generator-cli/src/app/app.module.spec.ts +++ b/apps/generator-cli/src/app/app.module.spec.ts @@ -146,6 +146,32 @@ describe('AppModule', () => { }); }); + describe.each([ + ['the separate option value', ['--openapitools', 'x.json']], + ['the inline option value', ['--openapitools=x.json']], + ])('version-manager resolve with %s', (_, configArgs) => { + beforeEach(async () => { + process.argv = [ + 'foo', + 'baz', + ...configArgs, + 'version-manager', + 'resolve', + 'latest', + ]; + programMock.parse.mockImplementation(() => undefined); + versionManagerServiceMock.getSelectedVersion.mockReturnValue('1.2.3'); + await fixture.onApplicationBootstrap(); + }); + + it('does not download or initialize the pass-through service', () => { + expect( + versionManagerServiceMock.downloadIfNeeded, + ).toHaveBeenCalledTimes(0); + expect(passThroughServiceMock.init).toHaveBeenCalledTimes(0); + }); + }); + describe('a pass-through command contains version-manager resolve as arguments', () => { beforeEach(async () => { process.argv = [