Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .changeset/config-defaults-and-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
"@asyncapi/cli": minor
---

Add per-command config defaults and authentication commands for private $refs

**New Features:**

1. **Config Defaults** (#1914): Set default flags for commands to avoid repetitive typing
- `asyncapi config defaults set <command> <flags>` - Set defaults for a command
- `asyncapi config defaults list` - List all configured defaults
- `asyncapi config defaults remove <command>` - Remove defaults for a command
- Defaults are automatically applied when running commands
- CLI flags still override defaults (precedence: CLI > defaults > oclif defaults)

2. **Auth Commands** (#1796): Configure authentication for private schema repositories
- `asyncapi config auth list` - List configured auth entries
- `asyncapi config auth remove <pattern>` - Remove auth configuration
- `asyncapi config auth test <url>` - Test URL pattern matching
- Tokens are stored as `${ENV_VAR}` templates and resolved at runtime
- Enables validation of AsyncAPI files with private $refs

**Examples:**

```bash
# Set defaults to avoid typing same flags
asyncapi config defaults set validate --log-diagnostics --fail-severity error
asyncapi validate test.yaml # Automatically uses defaults

# Configure authentication for private schemas
export GITHUB_TOKEN=ghp_your_token
asyncapi config auth add "https://github.com/myorg/*" '$GITHUB_TOKEN'
asyncapi validate asyncapi.yaml # Private $refs now work!
```

**Technical Details:**
- Zero breaking changes - all changes are additive
- 98% test coverage on ConfigService
- Backward compatible with existing config files
- Security: Tokens resolved from environment variables at runtime
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion scripts/fetch-asyncapi-example.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ const listAllProtocolsForFile = (document) => {
};

const tidyUp = async () => {
fs.unlinkSync(TEMP_ZIP_NAME);
if (fs.existsSync(TEMP_ZIP_NAME)) {
fs.unlinkSync(TEMP_ZIP_NAME);
}
};

(async () => {
Expand Down
47 changes: 47 additions & 0 deletions src/apps/cli/commands/config/auth/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Command from '@cli/internal/base';
import { ConfigService } from '@services/config.service';
import { helpFlag } from '@cli/internal/flags/global.flags';
import { cyan, blueBright } from 'picocolors';

export default class AuthList extends Command {
static readonly description = 'List configured authentication entries';

static readonly examples = [
'$ asyncapi config auth list',
];

static readonly flags = helpFlag();

async run() {
await this.parse(AuthList);

const config = await ConfigService.loadConfig();

if (!config.auth || config.auth.length === 0) {
this.log('No authentication configured.');
this.log('');
this.log('Add authentication with:');
this.log(cyan(' asyncapi config auth add --pattern <url-pattern> --type <type> --token-env <env-var>'));
return;
}

this.log(blueBright('Configured authentication:\\n'));

for (const [index, entry] of config.auth.entries()) {
this.log(cyan(`${index + 1}. ${entry.pattern}`));
this.log(` Type: ${entry.authType || 'Bearer'}`);
this.log(` Token: ${entry.token}`);

if (entry.headers && Object.keys(entry.headers).length > 0) {
this.log(' Custom headers:');
for (const [key, value] of Object.entries(entry.headers)) {
this.log(` ${key}: ${value}`);
}
}

this.log('');
}

this.log(cyan(`Total: ${config.auth.length} auth entries configured`));
}
}
45 changes: 45 additions & 0 deletions src/apps/cli/commands/config/auth/remove.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Args } from '@oclif/core';
import Command from '@cli/internal/base';
import { ConfigService } from '@services/config.service';
import { helpFlag } from '@cli/internal/flags/global.flags';
import { green } from 'picocolors';

export default class AuthRemove extends Command {
static readonly description = 'Remove authentication for a URL pattern';

static readonly examples = [
'$ asyncapi config auth remove "https://schema-registry.company.com/*"',
'$ asyncapi config auth remove "https://github.com/myorg/*"',
];

static readonly args = {
pattern: Args.string({
description: 'URL pattern to remove authentication for',
required: true,
}),
};

static readonly flags = helpFlag();

async run() {
const { args } = await this.parse(AuthRemove);
const pattern = args.pattern;

const config = await ConfigService.loadConfig();

if (!config.auth || config.auth.length === 0) {
this.error('No authentication configured.');
}

const initialLength = config.auth.length;
config.auth = config.auth.filter(entry => entry.pattern !== pattern);

if (config.auth.length === initialLength) {
this.error(`No authentication found for pattern: ${pattern}`);
}

await ConfigService.saveConfig(config);

this.log(green(`βœ“ Authentication removed for pattern: ${pattern}`));
}
}
60 changes: 60 additions & 0 deletions src/apps/cli/commands/config/auth/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Args } from '@oclif/core';
import Command from '@cli/internal/base';
import { ConfigService } from '@services/config.service';
import { helpFlag } from '@cli/internal/flags/global.flags';
import { green, yellow, cyan } from 'picocolors';

export default class AuthTest extends Command {
static readonly description = 'Test which auth entry matches a URL';

static readonly examples = [
'$ asyncapi config auth test "https://schema-registry.company.com/schemas/user.yaml"',
'$ asyncapi config auth test "https://github.com/myorg/repo/blob/main/schema.yaml"',
];

static readonly args = {
url: Args.string({
description: 'URL to test',
required: true,
}),
};

static readonly flags = helpFlag();

async run() {
const { args } = await this.parse(AuthTest);
const url = args.url;

this.log(`Testing URL: ${cyan(url)}\n`);

const authResult = await ConfigService.getAuthForUrl(url);

if (!authResult) {
this.log(yellow('βœ— No matching authentication found'));
this.log('');
this.log('Add authentication with:');
this.log(' asyncapi config auth add --pattern <url-pattern> --type <type> --token-env <env-var>');
return;
}

this.log(green('βœ“ Authentication found!'));
this.log('');
this.log(` Type: ${authResult.authType}`);

if (authResult.token) {
const displayToken = authResult.token.length > 10
? `${authResult.token.substring(0, 10)}...`
: authResult.token;
this.log(` Token: ${displayToken}`);
} else {
this.log(yellow(' Token: (not set - check environment variable)'));
}

if (Object.keys(authResult.headers).length > 0) {
this.log(' Headers:');
for (const [key, value] of Object.entries(authResult.headers)) {
this.log(` ${key}: ${value}`);
}
}
}
}
38 changes: 38 additions & 0 deletions src/apps/cli/commands/config/defaults/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Command from '@cli/internal/base';
import { ConfigService } from '@services/config.service';
import { helpFlag } from '@cli/internal/flags/global.flags';
import { blueBright, cyan } from 'picocolors';

export default class DefaultsList extends Command {
static readonly description = 'List all configured command defaults';

static readonly examples = [
'$ asyncapi config defaults list',
];

static readonly flags = helpFlag();

async run() {
await this.parse(DefaultsList);

const allDefaults = await ConfigService.listAllDefaults();

if (Object.keys(allDefaults).length === 0) {
this.log('No command defaults configured.');
this.log('');
this.log('Set defaults with:');
this.log(` ${cyan('asyncapi config defaults set <command> [flags]')}`);
return;
}

this.log('Configured command defaults:\n');

for (const [commandId, defaults] of Object.entries(allDefaults)) {
this.log(`${blueBright(commandId)}:`);
this.log(JSON.stringify(defaults, null, 2));
this.log('');
}

this.log(cyan(`Total: ${Object.keys(allDefaults).length} commands configured`));
}
}
38 changes: 38 additions & 0 deletions src/apps/cli/commands/config/defaults/remove.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Args } from '@oclif/core';
import Command from '@cli/internal/base';
import { ConfigService } from '@services/config.service';
import { helpFlag } from '@cli/internal/flags/global.flags';
import { green } from 'picocolors';

export default class DefaultsRemove extends Command {
static readonly description = 'Remove defaults for a command';

static readonly examples = [
'$ asyncapi config defaults remove validate',
'$ asyncapi config defaults remove generate:fromTemplate',
];

static readonly args = {
command: Args.string({
description: 'Command to remove defaults for',
required: true,
}),
};

static readonly flags = helpFlag();

async run() {
const { args } = await this.parse(DefaultsRemove);
const commandId = args.command;

const allDefaults = await ConfigService.listAllDefaults();

if (!allDefaults[commandId]) {
this.error(`No defaults configured for command "${commandId}"`);
}

await ConfigService.removeCommandDefaults(commandId);

this.log(green(`βœ“ Defaults removed for command "${commandId}"`));
}
}
83 changes: 83 additions & 0 deletions src/apps/cli/commands/config/defaults/set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Args } from '@oclif/core';
import Command from '@cli/internal/base';
import { ConfigService } from '@services/config.service';
import { helpFlag } from '@cli/internal/flags/global.flags';
import { green, cyan, yellow } from 'picocolors';

export default class DefaultsSet extends Command {
static readonly description = 'Set default flags for a command';

static readonly examples = [
'$ asyncapi config defaults set validate --log-diagnostics --fail-severity error',
'$ asyncapi config defaults set generate:fromTemplate --template @asyncapi/html-template --output ./docs',
'$ asyncapi config defaults set bundle --output ./dist/bundled.yaml',
];

static readonly args = {
command: Args.string({
description: 'Command to set defaults for (e.g., validate, generate:fromTemplate)',
required: true,
}),
};

static readonly flags = helpFlag();

static readonly strict = false;
static readonly enableJsonFlag = false;

async run() {
const rawArgv = process.argv.slice(2);

const setIndex = rawArgv.indexOf('set');
if (setIndex === -1 || setIndex + 1 >= rawArgv.length) {
this.error('Command argument required');
}

const commandId = rawArgv[setIndex + 1];
const flagArgs = rawArgv.slice(setIndex + 2);

if (flagArgs.length === 0) {
this.error('No flags provided. Specify at least one flag to set as default.\\n\\nExample:\\n asyncapi config defaults set validate --log-diagnostics --fail-severity error');
}

const defaults: Record<string, any> = {};

let i = 0;
while (i < flagArgs.length) {
const arg = flagArgs[i];

if (!arg.startsWith('--')) {
this.warn(yellow(`Skipping non-flag argument: ${arg}`));
i++;
continue;
}

const flagName = arg.replace(/^--/, '');

if (flagName === 'help') {
i++;
continue;
}

if (i + 1 < flagArgs.length && !flagArgs[i + 1].startsWith('--')) {
defaults[flagName] = flagArgs[i + 1];
i += 2;
} else {
defaults[flagName] = true;
i++;
}
}

if (Object.keys(defaults).length === 0) {
this.error('No valid flags provided. Specify at least one flag to set as default.');
}

await ConfigService.setCommandDefaults(commandId, defaults);

this.log(green(`βœ“ Defaults set for command "${commandId}":`));
this.log(JSON.stringify(defaults, null, 2));
this.log('');
this.log(cyan('These defaults will be automatically applied when running the command.'));
this.log(cyan('CLI flags will still override these defaults.'));
}
}
Loading