Skip to content

Commit 7fa1ad4

Browse files
committed
refactoring configuration loading
1 parent 1b4363c commit 7fa1ad4

File tree

16 files changed

+1548
-279
lines changed

16 files changed

+1548
-279
lines changed

packages/b2c-tooling-sdk/package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,17 @@
144144
"types": "./dist/cjs/errors/index.d.ts",
145145
"default": "./dist/cjs/errors/index.js"
146146
}
147+
},
148+
"./config": {
149+
"development": "./src/config/index.ts",
150+
"import": {
151+
"types": "./dist/esm/config/index.d.ts",
152+
"default": "./dist/esm/config/index.js"
153+
},
154+
"require": {
155+
"types": "./dist/cjs/config/index.d.ts",
156+
"default": "./dist/cjs/config/index.js"
157+
}
147158
}
148159
},
149160
"main": "./dist/cjs/index.js",

packages/b2c-tooling-sdk/src/cli/config.ts

Lines changed: 57 additions & 210 deletions
Original file line numberDiff line numberDiff line change
@@ -3,245 +3,92 @@
33
* SPDX-License-Identifier: Apache-2
44
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
55
*/
6+
/**
7+
* CLI configuration utilities.
8+
*
9+
* This module provides configuration loading for CLI commands.
10+
* It uses the ConfigResolver internally for consistent behavior.
11+
*
12+
* @module cli/config
13+
*/
614
import * as fs from 'node:fs';
715
import * as os from 'node:os';
816
import * as path from 'node:path';
917
import type {AuthMethod} from '../auth/types.js';
1018
import {ALL_AUTH_METHODS} from '../auth/types.js';
19+
import {createConfigResolver, type NormalizedConfig} from '../config/index.js';
20+
import {findDwJson} from '../config/dw-json.js';
1121
import {getLogger} from '../logging/logger.js';
1222

1323
// Re-export for convenience
1424
export type {AuthMethod};
1525
export {ALL_AUTH_METHODS};
16-
17-
export interface ResolvedConfig {
18-
hostname?: string;
19-
webdavHostname?: string;
20-
codeVersion?: string;
21-
username?: string;
22-
password?: string;
23-
clientId?: string;
24-
clientSecret?: string;
25-
scopes?: string[];
26-
shortCode?: string;
27-
mrtApiKey?: string;
28-
/** MRT project slug */
29-
mrtProject?: string;
30-
/** MRT environment name (e.g., staging, production) */
31-
mrtEnvironment?: string;
32-
/** MRT API origin URL override */
33-
mrtOrigin?: string;
34-
instanceName?: string;
35-
/** Allowed authentication methods (in priority order). If not set, all methods are allowed. */
36-
authMethods?: AuthMethod[];
37-
}
26+
export {findDwJson};
3827

3928
/**
40-
* dw.json single config structure
29+
* Resolved configuration for CLI commands.
30+
*
31+
* This type is an alias for NormalizedConfig to maintain backward compatibility
32+
* with existing CLI code. It may be extended with CLI-specific fields in the future.
4133
*/
42-
interface DwJsonConfig {
43-
name?: string;
44-
active?: boolean;
45-
hostname?: string;
46-
'code-version'?: string;
47-
username?: string;
48-
password?: string;
49-
'client-id'?: string;
50-
'client-secret'?: string;
51-
'oauth-scopes'?: string[];
52-
/** SCAPI short code (multiple key formats supported) */
53-
shortCode?: string;
54-
'short-code'?: string;
55-
'scapi-shortcode'?: string;
56-
secureHostname?: string;
57-
'secure-server'?: string;
58-
/** Allowed authentication methods (in priority order) */
59-
'auth-methods'?: AuthMethod[];
60-
/** MRT project slug */
61-
mrtProject?: string;
62-
/** MRT environment name (e.g., staging, production) */
63-
mrtEnvironment?: string;
64-
}
34+
export type ResolvedConfig = NormalizedConfig;
6535

6636
/**
67-
* dw.json with multi-config support
37+
* Options for loading configuration.
6838
*/
69-
interface DwJsonMultiConfig extends DwJsonConfig {
70-
configs?: DwJsonConfig[];
71-
}
72-
7339
export interface LoadConfigOptions {
40+
/** Named instance from dw.json "configs" array */
7441
instance?: string;
42+
/** Explicit path to config file (skips searching if provided) */
7543
configPath?: string;
44+
/** Cloud origin for MRT ~/.mobify lookup (e.g., https://cloud-staging.mobify.com) */
45+
cloudOrigin?: string;
7646
}
7747

7848
/**
79-
* Finds dw.json by walking up from current directory.
80-
*/
81-
export function findDwJson(startDir: string = process.cwd()): string | null {
82-
const logger = getLogger();
83-
let dir = startDir;
84-
const root = path.parse(dir).root;
85-
86-
logger.trace({startDir}, '[Config] Searching for dw.json');
87-
88-
while (dir !== root) {
89-
const dwJsonPath = path.join(dir, 'dw.json');
90-
if (fs.existsSync(dwJsonPath)) {
91-
logger.trace({path: dwJsonPath}, '[Config] Found dw.json');
92-
return dwJsonPath;
93-
}
94-
dir = path.dirname(dir);
95-
}
96-
97-
logger.trace('[Config] No dw.json found');
98-
return null;
99-
}
100-
101-
/**
102-
* Maps dw.json fields to ResolvedConfig
103-
*/
104-
function mapDwJsonToConfig(json: DwJsonConfig): ResolvedConfig {
105-
return {
106-
hostname: json.hostname,
107-
webdavHostname: json.secureHostname || json['secure-server'],
108-
codeVersion: json['code-version'],
109-
username: json.username,
110-
password: json.password,
111-
clientId: json['client-id'],
112-
clientSecret: json['client-secret'],
113-
scopes: json['oauth-scopes'],
114-
shortCode: json.shortCode || json['short-code'] || json['scapi-shortcode'],
115-
instanceName: json.name,
116-
authMethods: json['auth-methods'],
117-
mrtProject: json.mrtProject,
118-
mrtEnvironment: json.mrtEnvironment,
119-
};
120-
}
121-
122-
/**
123-
* Loads configuration from dw.json file.
124-
* Supports multi-config format with 'configs' array.
125-
*/
126-
function loadDwJson(instanceName?: string, configPath?: string): ResolvedConfig {
127-
const logger = getLogger();
128-
const dwJsonPath = configPath || findDwJson();
129-
130-
if (!dwJsonPath || !fs.existsSync(dwJsonPath)) {
131-
logger.trace('[Config] No dw.json to load');
132-
return {};
133-
}
134-
135-
try {
136-
const content = fs.readFileSync(dwJsonPath, 'utf8');
137-
const json = JSON.parse(content) as DwJsonMultiConfig;
138-
139-
let selectedConfig: DwJsonConfig = json;
140-
let selectedName = json.name || 'root';
141-
142-
// Handle multi-config format
143-
if (Array.isArray(json.configs)) {
144-
if (instanceName) {
145-
// Find by instance name
146-
const found = json.name === instanceName ? json : json.configs.find((c) => c.name === instanceName);
147-
if (found) {
148-
selectedConfig = found;
149-
selectedName = found.name || instanceName;
150-
}
151-
} else if (json.active === false) {
152-
// Root config is inactive, find active one in configs
153-
const activeConfig = json.configs.find((c) => c.active === true);
154-
if (activeConfig) {
155-
selectedConfig = activeConfig;
156-
selectedName = activeConfig.name || 'active';
157-
}
158-
}
159-
// Otherwise use root config
160-
}
161-
162-
logger.trace({path: dwJsonPath, instance: selectedName}, '[Config] Loaded dw.json');
163-
return mapDwJsonToConfig(selectedConfig);
164-
} catch (error) {
165-
logger.trace({path: dwJsonPath, error}, '[Config] Failed to parse dw.json');
166-
return {};
167-
}
168-
}
169-
170-
/**
171-
* Merges config sources with precedence: flags (includes env via OCLIF) > dw.json
49+
* Loads configuration with precedence: CLI flags/env vars > dw.json
50+
*
51+
* OCLIF handles environment variables automatically via flag `env` properties.
52+
* The flags parameter already contains resolved env var values.
53+
*
54+
* Uses ConfigResolver internally for consistent behavior across CLI and SDK.
17255
*
173-
* Note: Environment variables are handled by OCLIF's flag parsing with the `env`
174-
* property on each flag definition. By the time flags reach this function, they
175-
* already contain env var values where applicable.
56+
* @param flags - Configuration values from CLI flags/env vars
57+
* @param options - Loading options
58+
* @returns Resolved configuration
17659
*
177-
* IMPORTANT: If the hostname is explicitly provided (via flags/env) and differs
178-
* from the dw.json hostname, we do NOT use ANY configuration from dw.json since
179-
* the dw.json is configured for a different server.
60+
* @example
61+
* ```typescript
62+
* // In a CLI command
63+
* const config = loadConfig(
64+
* { hostname: this.flags.server, clientId: this.flags['client-id'] },
65+
* { instance: this.flags.instance }
66+
* );
67+
* ```
18068
*/
181-
function mergeConfigs(
182-
flags: Partial<ResolvedConfig>,
183-
dwJson: ResolvedConfig,
184-
options: LoadConfigOptions,
185-
): ResolvedConfig {
69+
export function loadConfig(flags: Partial<ResolvedConfig> = {}, options: LoadConfigOptions = {}): ResolvedConfig {
18670
const logger = getLogger();
187-
188-
// Check if hostname was explicitly provided and differs from dw.json
189-
const hostnameExplicitlyProvided = Boolean(flags.hostname);
190-
const hostnameMismatch = hostnameExplicitlyProvided && dwJson.hostname && flags.hostname !== dwJson.hostname;
191-
192-
// If hostname mismatch, ignore dw.json entirely
193-
if (hostnameMismatch) {
194-
logger.trace(
195-
{providedHostname: flags.hostname, dwJsonHostname: dwJson.hostname, ignoredConfig: dwJson},
196-
'[Config] Hostname mismatch - ignoring dw.json configuration',
197-
);
198-
return {
199-
hostname: flags.hostname,
200-
webdavHostname: flags.webdavHostname,
201-
codeVersion: flags.codeVersion,
202-
username: flags.username,
203-
password: flags.password,
204-
clientId: flags.clientId,
205-
clientSecret: flags.clientSecret,
206-
scopes: flags.scopes,
207-
shortCode: flags.shortCode,
208-
mrtApiKey: flags.mrtApiKey,
209-
mrtProject: flags.mrtProject,
210-
mrtEnvironment: flags.mrtEnvironment,
211-
mrtOrigin: flags.mrtOrigin,
212-
instanceName: undefined,
213-
authMethods: flags.authMethods,
214-
};
71+
const resolver = createConfigResolver();
72+
73+
const {config, warnings} = resolver.resolve(flags, {
74+
instance: options.instance,
75+
configPath: options.configPath,
76+
hostnameProtection: true,
77+
cloudOrigin: options.cloudOrigin,
78+
});
79+
80+
// Log warnings
81+
for (const warning of warnings) {
82+
logger.trace({warning}, `[Config] ${warning.message}`);
21583
}
21684

217-
return {
218-
hostname: flags.hostname || dwJson.hostname,
219-
webdavHostname: flags.webdavHostname || dwJson.webdavHostname,
220-
codeVersion: flags.codeVersion || dwJson.codeVersion,
221-
username: flags.username || dwJson.username,
222-
password: flags.password || dwJson.password,
223-
clientId: flags.clientId || dwJson.clientId,
224-
clientSecret: flags.clientSecret || dwJson.clientSecret,
225-
scopes: flags.scopes || dwJson.scopes,
226-
shortCode: flags.shortCode || dwJson.shortCode,
227-
mrtApiKey: flags.mrtApiKey,
228-
mrtProject: flags.mrtProject || dwJson.mrtProject,
229-
mrtEnvironment: flags.mrtEnvironment || dwJson.mrtEnvironment,
230-
mrtOrigin: flags.mrtOrigin,
231-
instanceName: dwJson.instanceName || options.instance,
232-
authMethods: flags.authMethods || dwJson.authMethods,
233-
};
234-
}
85+
// Handle instanceName from options if not in resolved config
86+
// This preserves backward compatibility with the old behavior
87+
if (!config.instanceName && options.instance) {
88+
config.instanceName = options.instance;
89+
}
23590

236-
/**
237-
* Loads configuration with precedence: CLI flags/env vars > dw.json
238-
*
239-
* OCLIF handles environment variables automatically via flag `env` properties.
240-
* The flags parameter already contains resolved env var values.
241-
*/
242-
export function loadConfig(flags: Partial<ResolvedConfig> = {}, options: LoadConfigOptions = {}): ResolvedConfig {
243-
const dwJsonConfig = loadDwJson(options.instance, options.configPath);
244-
return mergeConfigs(flags, dwJsonConfig, options);
91+
return config as ResolvedConfig;
24592
}
24693

24794
/**

packages/b2c-tooling-sdk/src/cli/instance-command.ts

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import {Command, Flags} from '@oclif/core';
77
import {OAuthCommand} from './oauth-command.js';
88
import {loadConfig} from './config.js';
99
import type {ResolvedConfig, LoadConfigOptions} from './config.js';
10-
import {B2CInstance} from '../instance/index.js';
11-
import type {AuthConfig} from '../auth/types.js';
10+
import {createInstanceFromConfig} from '../config/index.js';
11+
import type {B2CInstance} from '../instance/index.js';
1212
import {t} from '../i18n/index.js';
1313

1414
/**
@@ -89,6 +89,7 @@ export abstract class InstanceCommand<T extends typeof Command> extends OAuthCom
8989
clientId: this.flags['client-id'],
9090
clientSecret: this.flags['client-secret'],
9191
authMethods: this.parseAuthMethods(),
92+
accountManagerHost: this.flags['account-manager-host'],
9293
};
9394

9495
const config = loadConfig(flagConfig, options);
@@ -117,38 +118,7 @@ export abstract class InstanceCommand<T extends typeof Command> extends OAuthCom
117118
protected get instance(): B2CInstance {
118119
if (!this._instance) {
119120
this.requireServer();
120-
121-
const config = this.resolvedConfig;
122-
123-
const authConfig: AuthConfig = {
124-
authMethods: config.authMethods,
125-
};
126-
127-
if (config.username && config.password) {
128-
authConfig.basic = {
129-
username: config.username,
130-
password: config.password,
131-
};
132-
}
133-
134-
// Only require clientId for OAuth - clientSecret is optional for implicit flow
135-
if (config.clientId) {
136-
authConfig.oauth = {
137-
clientId: config.clientId,
138-
clientSecret: config.clientSecret,
139-
scopes: config.scopes,
140-
accountManagerHost: this.accountManagerHost,
141-
};
142-
}
143-
144-
this._instance = new B2CInstance(
145-
{
146-
hostname: config.hostname!,
147-
codeVersion: config.codeVersion,
148-
webdavHostname: config.webdavHostname,
149-
},
150-
authConfig,
151-
);
121+
this._instance = createInstanceFromConfig(this.resolvedConfig);
152122
}
153123
return this._instance;
154124
}

0 commit comments

Comments
 (0)