Skip to content
8 changes: 5 additions & 3 deletions apps/generator-cli/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,14 @@ 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 <versionTags...>`
- `openapi-generator-cli version-manager resolve <versionTags...>`
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

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

Expand Down Expand Up @@ -307,5 +311,3 @@ yarn add @openapitools/openapi-generator-cli@1.0.18-4.3.1

Please leave a star.



76 changes: 76 additions & 0 deletions apps/generator-cli/src/app/app.module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,82 @@ 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.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 = [
'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 = [
Expand Down
34 changes: 32 additions & 2 deletions apps/generator-cli/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -81,7 +83,35 @@ 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);
let commandIndex = 0;

while (commandIndex < args.length) {
const arg = args[commandIndex];

if (arg === '--openapitools') {
commandIndex += 2;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
continue;
}

if (arg.startsWith('--openapitools=')) {
commandIndex += 1;
continue;
}

break;
}

return (
args[commandIndex] === 'version-manager' &&
args[commandIndex + 1] === 'resolve'
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('VersionManagerController', () => {
let commandMock: CommandMock;

const log = jest.fn();
const error = jest.fn();

const uiServiceMock = {
table: jest.fn(),
Expand All @@ -32,6 +33,7 @@ describe('VersionManagerController', () => {

[
log,
error,
...Object.values(uiServiceMock),
...Object.values(versionManagerServiceMock),
].forEach((spy) => spy.mockReset());
Expand All @@ -42,15 +44,15 @@ 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();
});

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 () {
Expand Down Expand Up @@ -310,5 +312,44 @@ describe('VersionManagerController', () => {
);
});
});

describe('resolve', () => {
beforeEach(() => {
cmd = commandMock.commands['resolve <versionTags...>'];
});

afterEach(() => {
process.exitCode = 0;
});

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.4' }, { version: '1.2.3' }]),
);

await cmd.action(['latest']);

expect(versionManagerServiceMock.search).toHaveBeenNthCalledWith(1, [
'latest',
]);
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 () => {
versionManagerServiceMock.search.mockReturnValue(of([]));

await cmd.action(['latest']);

expect(error).toHaveBeenNthCalledWith(
1,
chalk.red('Unable to find version matching criteria "latest"'),
);
expect(process.exitCode).toBe(1);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export class VersionManagerController {
.description('set version to use')
.action((tags) => this.set(tags));

private readonly resolveCommand = this.mainCommand
.command('resolve <versionTags...>')
.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,
Expand Down Expand Up @@ -95,6 +100,22 @@ 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.error(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This edit lands in set, not resolve. set's no-match message now goes to stderr, a behavior change nobody asked for, and the set spec still expects log, so it will fail. Revert this line to this.logger.log(; resolve already uses logger.error.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/generator-cli/src/app/controllers/version-manager.controller.ts, line 111:

<comment>This edit lands in `set`, not `resolve`. `set`'s no-match message now goes to stderr, a behavior change nobody asked for, and the `set` spec still expects `log`, so it will fail. Revert this line to `this.logger.log(`; `resolve` already uses `logger.error`.</comment>

<file context>
@@ -108,7 +108,7 @@ export class VersionManagerController {
     }
 
-    this.logger.log(
+    this.logger.error(
       chalk.red(
         `Unable to find version matching criteria "${versionTags.join(' ')}"`,
</file context>
Suggested change
this.logger.error(
this.logger.log(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be stale or misattributed. In the current code, set still uses logger.log, while only resolve uses logger.error, so no further change is needed.

chalk.red(
`Unable to find version matching criteria "${versionTags.join(' ')}"`,
),
);
process.exitCode = 1;
};

private table = (versions: Version[]) => {
// Keep row order identical to the order returned in versions.
const rows = versions.map((version) => {
Expand Down
14 changes: 9 additions & 5 deletions apps/generator-cli/src/app/services/config.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -69,12 +75,10 @@ describe('ConfigService', () => {
});
});

it('ensures the config file', () => {
it('does not create or modify the config file', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This regression test does not cover the absent-config case: its setup always returns an existing configuration object. Add a case with fs.readJSONSync.mockReturnValue(undefined) and assert that ensureFileSync and the write methods remain unused, so a missing openapitools.json cannot regress to being created.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/generator-cli/src/app/services/config.service.spec.ts, line 72:

<comment>This regression test does not cover the absent-config case: its setup always returns an existing configuration object. Add a case with `fs.readJSONSync.mockReturnValue(undefined)` and assert that `ensureFileSync` and the write methods remain unused, so a missing `openapitools.json` cannot regress to being created.</comment>

<file context>
@@ -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(
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d7f0e8f. The tests now cover an absent configuration (readJSONSync returns undefined) and verify that no file creation or write occurs.

fixture.get('some-path');
expect(fs.ensureFileSync).toHaveBeenNthCalledWith(
1,
fixture.configFile
);
expect(fs.ensureFileSync).toHaveBeenCalledTimes(0);
expect(fs.writeJSONSync).toHaveBeenCalledTimes(0);
});

it.each([
Expand Down
3 changes: 1 addition & 2 deletions apps/generator-cli/src/app/services/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,6 @@ export class ConfigService {
return result;
};

fs.ensureFileSync(this.configFile);

const fileConfig =
fs.readJSONSync(this.configFile, { throws: false, encoding: 'utf8' }) ??
{};
Expand Down Expand Up @@ -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})
}
}
Loading