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
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"prepare": "tsc -b",
"build": "tsc -b",
"dev": "tsc -b --watch",
"test": "vitest run",
"typecheck": "tsc -b --noEmit",
"test": "npm run typecheck && vitest run",
"test:watch": "vitest",
"lint": "eslint src",
"postpack": "rm -f oclif.manifest.json",
Expand Down Expand Up @@ -94,6 +95,9 @@
"config:list": {
"description": "List configuration resources"
},
"config:profile": {
"description": "Manage configuration profiles"
},
"encoding": {
"description": "Encoding operations"
},
Expand Down
8 changes: 4 additions & 4 deletions src/commands/config/list/organizations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import chalk from 'chalk';
import {BaseCommand} from '../../../lib/base-command.js';
import {loadConfig} from '../../../lib/config.js';
import {loadConfig, getProfile} from '../../../lib/config.js';

export default class ConfigListOrganizations extends BaseCommand {
static override description = 'List available organizations and optionally select one';
Expand All @@ -16,19 +16,19 @@ export default class ConfigListOrganizations extends BaseCommand {
];

async run(): Promise<void> {
const config = loadConfig();
const profile = getProfile(loadConfig());
const result = await (await this.getApi()).account.organizations.list();
const orgs = result.items ?? [];

// Collect all orgs including sub-orgs for structured output
const allOrgs: Record<string, unknown>[] = [];
for (const org of orgs) {
allOrgs.push({id: org.id, name: org.name, active: config.tenantOrgId === org.id, parent: null});
allOrgs.push({id: org.id, name: org.name, active: profile.tenantOrgId === org.id, parent: null});
if (org.id) {
try {
const subOrgs = await (await this.getApi()).account.organizations.subOrganizations.list(org.id);
for (const sub of (subOrgs.items ?? [])) {
allOrgs.push({id: sub.id, name: sub.name, active: config.tenantOrgId === sub.id, parent: org.id});
allOrgs.push({id: sub.id, name: sub.name, active: profile.tenantOrgId === sub.id, parent: org.id});
}
} catch {
// Sub-organizations may not be accessible
Expand Down
29 changes: 29 additions & 0 deletions src/commands/config/profile/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {Args} from '@oclif/core';
import {BaseCommand} from '../../../lib/base-command.js';
import {deleteProfile} from '../../../lib/config.js';

export default class ConfigProfileDelete extends BaseCommand {
static override description = 'Delete a configuration profile';

static override flags = {
...BaseCommand.baseFlags,
};

static override args = {
name: Args.string({description: 'Profile name to delete', required: true}),
};

static override examples = [
'bitmovin config profile delete staging',
];

async run(): Promise<void> {
const {args} = await this.parse(ConfigProfileDelete);
try {
deleteProfile(args.name);
this.log(`Deleted profile: ${args.name}`);
} catch (err) {
this.error((err as Error).message);
}
}
}
53 changes: 53 additions & 0 deletions src/commands/config/profile/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import chalk from 'chalk';
import {BaseCommand} from '../../../lib/base-command.js';
import {listProfiles} from '../../../lib/config.js';

export default class ConfigProfileList extends BaseCommand {
static override description = 'List all configuration profiles';

static override flags = {
...BaseCommand.baseFlags,
};

static override examples = [
'bitmovin config profile list',
'bitmovin config profile list --json',
];

async run(): Promise<void> {
const profiles = listProfiles();

if (await this.isJsonMode()) {
await this.outputList(
profiles.map(p => ({
name: p.name,
active: p.active,
apiKey: p.config.apiKey ? p.config.apiKey.slice(0, 8) + '...' : null,
tenantOrgId: p.config.tenantOrgId ?? null,
defaultRegion: p.config.defaultRegion ?? null,
})),
['name', 'active', 'apiKey', 'tenantOrgId', 'defaultRegion'],
);
return;
}

if (profiles.length === 0) {
this.log('No profiles configured.');
return;
}

const lines: string[] = [''];
for (const p of profiles) {
const activeMarker = p.active ? chalk.green(' (active)') : '';
const keyStr = p.config.apiKey
? chalk.dim(p.config.apiKey.slice(0, 8) + '...')
: chalk.dim('no api key');
const orgStr = p.config.tenantOrgId ? chalk.dim(` org: ${p.config.tenantOrgId}`) : '';
lines.push(` ${chalk.bold(p.name)}${activeMarker} ${keyStr}${orgStr}`);
}

lines.push('');
lines.push(chalk.dim('Switch profile: bitmovin config profile use <name>'));
process.stdout.write(lines.join('\n') + '\n');
}
}
35 changes: 35 additions & 0 deletions src/commands/config/profile/use.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {Args} from '@oclif/core';
import {BaseCommand} from '../../../lib/base-command.js';
import {setActiveProfile} from '../../../lib/config.js';

export default class ConfigProfileUse extends BaseCommand {
static override description = 'Switch the active configuration profile';

static override flags = {
...BaseCommand.baseFlags,
};

static override args = {
name: Args.string({description: 'Profile name to activate', required: true}),
};

static override examples = [
'bitmovin config profile use production',
'bitmovin config profile use default',
];

async run(): Promise<void> {
const {args} = await this.parse(ConfigProfileUse);
try {
setActiveProfile(args.name);
this.log(`Switched to profile: ${args.name}`);
if (process.env.BITMOVIN_API_KEY) {
this.warn(
`BITMOVIN_API_KEY is set in your environment and will override the api-key from the '${args.name}' profile.`,
);
}
} catch (err) {
this.error((err as Error).message);
}
}
}
21 changes: 15 additions & 6 deletions src/commands/config/set.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Args} from '@oclif/core';
import {Args, Flags} from '@oclif/core';
import {BaseCommand} from '../../lib/base-command.js';
import {loadConfig, saveConfig} from '../../lib/config.js';

Expand All @@ -22,24 +22,33 @@ export default class ConfigSet extends BaseCommand {

static override flags = {
...BaseCommand.baseFlags,
profile: Flags.string({description: 'Profile to set the value in (default: active profile)'}),
};

static override examples = [
'bitmovin config set api-key 41514766-1f1a-480b-aafd-9c89a98932e8',
'bitmovin config set organization 5a1b2c3d-...',
'bitmovin config set api-key my-key --profile production',
'bitmovin config set default-region GOOGLE_EUROPE_WEST_1',
];

async run(): Promise<void> {
const {args} = await this.parse(ConfigSet);
const {args, flags} = await this.parse(ConfigSet);
const configKey = VALID_KEYS[args.key];
if (!configKey) {
this.error(`Unknown key: ${args.key}. Valid keys: ${Object.keys(VALID_KEYS).join(', ')}`);
}

const config = loadConfig();
(config as Record<string, string>)[configKey] = args.value;
saveConfig(config);
this.log(`Set ${args.key} = ${args.key === 'api-key' ? args.value.slice(0, 8) + '...' : args.value}`);
const full = loadConfig();
const profileName = flags.profile ?? full.activeProfile;
if (!full.profiles[profileName]) {
full.profiles[profileName] = {};
}

(full.profiles[profileName] as Record<string, string>)[configKey] = args.value;
saveConfig(full);

const suffix = profileName !== 'default' ? ` (profile: ${profileName})` : '';
this.log(`Set ${args.key} = ${args.key === 'api-key' ? args.value.slice(0, 8) + '...' : args.value}${suffix}`);
}
}
12 changes: 10 additions & 2 deletions src/commands/config/show.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
import {Flags} from '@oclif/core';
import {BaseCommand} from '../../lib/base-command.js';
import {loadConfig, getConfigPath} from '../../lib/config.js';
import {loadConfig, getProfile, getConfigPath} from '../../lib/config.js';

export default class ConfigShow extends BaseCommand {
static override description = 'Show current configuration';

static override flags = {
...BaseCommand.baseFlags,
profile: Flags.string({description: 'Profile to show (default: active profile)'}),
};

async run(): Promise<void> {
const config = loadConfig();
const {flags} = await this.parse(ConfigShow);
const full = loadConfig();
const profileName = flags.profile ?? full.activeProfile;
const config = getProfile(full, profileName);

if (await this.isJsonMode()) {
const masked = config.apiKey
? config.apiKey.slice(0, 8) + '...' + config.apiKey.slice(-4)
: undefined;
await this.outputData({
configFile: getConfigPath(),
activeProfile: full.activeProfile,
profile: profileName,
apiKey: masked ?? '(not set)',
tenantOrgId: config.tenantOrgId ?? '(not set)',
defaultRegion: config.defaultRegion ?? '(not set)',
Expand All @@ -25,6 +32,7 @@ export default class ConfigShow extends BaseCommand {
}

this.log(`Config file: ${getConfigPath()}\n`);
this.log(`Profile: ${profileName}${profileName === full.activeProfile ? ' (active)' : ''}`);

if (config.apiKey) {
const masked = config.apiKey.slice(0, 8) + '...' + config.apiKey.slice(-4);
Expand Down
14 changes: 9 additions & 5 deletions src/lib/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import chalk from 'chalk';
import {getClient, type ApiClient} from './client.js';
import {formatJson, formatTable, formatKeyValue, isTTY} from './output.js';
import {applyJq} from './jq.js';
import {loadConfig} from './config.js';
import {loadConfig, getProfile} from './config.js';

export abstract class BaseCommand extends Command {
static baseFlags = {
Expand All @@ -24,6 +24,7 @@ export abstract class BaseCommand extends Command {
hidden: false,
}),
'api-key': Flags.string({description: 'Override API key'}),
profile: Flags.string({description: 'Configuration profile to use', env: 'BITMOVIN_PROFILE'}),
quiet: Flags.boolean({char: 'q', description: 'Suppress non-essential output'}),
};

Expand All @@ -47,7 +48,7 @@ export abstract class BaseCommand extends Command {
protected override async catch(err: Error & {httpStatusCode?: number; errorCode?: number; developerMessage?: string; requestId?: string}): Promise<void> {
// Handle Bitmovin API errors
if (err.httpStatusCode) {
const config = loadConfig();
const profile = getProfile(loadConfig(), this._parsedFlags?.profile as string | undefined);
const lines: string[] = [];

switch (err.httpStatusCode) {
Expand All @@ -61,8 +62,8 @@ export abstract class BaseCommand extends Command {
case 403:
lines.push(chalk.red('Access denied.'));
lines.push('');
if (config.tenantOrgId) {
lines.push(` Active organization: ${config.tenantOrgId}`);
if (profile.tenantOrgId) {
lines.push(` Active organization: ${profile.tenantOrgId}`);
lines.push(' This organization may not have access to this resource.');
lines.push('');
lines.push(' Try switching organizations:');
Expand Down Expand Up @@ -131,7 +132,10 @@ export abstract class BaseCommand extends Command {
protected async getApi(): Promise<ApiClient> {
if (!this._api) {
const flags = await this.parseFlags();
this._api = getClient(flags['api-key'] as string | undefined);
this._api = getClient(
flags['api-key'] as string | undefined,
flags.profile as string | undefined,
);
}

return this._api;
Expand Down
10 changes: 5 additions & 5 deletions src/lib/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import BitmovinApiSdk from '@bitmovin/api-sdk';
import {loadConfig} from './config.js';
import {loadConfig, getProfile} from './config.js';

// The Bitmovin SDK is CJS with `export default class BitmovinApi`.
// Under NodeNext module resolution, TypeScript treats default imports from CJS
Expand Down Expand Up @@ -30,9 +30,9 @@ export interface ApiClient {
const SdkModule = BitmovinApiSdk as unknown as {default?: BitmovinApiConstructor};
const BitmovinApi: BitmovinApiConstructor = SdkModule.default ?? (BitmovinApiSdk as unknown as BitmovinApiConstructor);

export function getClient(apiKeyOverride?: string): ApiClient {
const config = loadConfig();
const apiKey = apiKeyOverride ?? process.env.BITMOVIN_API_KEY ?? config.apiKey;
export function getClient(apiKeyOverride?: string, profileName?: string): ApiClient {
const profile = getProfile(loadConfig(), profileName);
const apiKey = apiKeyOverride ?? process.env.BITMOVIN_API_KEY ?? profile.apiKey;

if (!apiKey) {
throw new Error(
Expand All @@ -45,6 +45,6 @@ export function getClient(apiKeyOverride?: string): ApiClient {

return new BitmovinApi({
apiKey,
...(config.tenantOrgId && {tenantOrgId: config.tenantOrgId}),
...(profile.tenantOrgId && {tenantOrgId: profile.tenantOrgId}),
});
}
Loading
Loading