|
3 | 3 | * SPDX-License-Identifier: Apache-2 |
4 | 4 | * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 |
5 | 5 | */ |
| 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 | + */ |
6 | 14 | import * as fs from 'node:fs'; |
7 | 15 | import * as os from 'node:os'; |
8 | 16 | import * as path from 'node:path'; |
9 | 17 | import type {AuthMethod} from '../auth/types.js'; |
10 | 18 | 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'; |
11 | 21 | import {getLogger} from '../logging/logger.js'; |
12 | 22 |
|
13 | 23 | // Re-export for convenience |
14 | 24 | export type {AuthMethod}; |
15 | 25 | 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}; |
38 | 27 |
|
39 | 28 | /** |
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. |
41 | 33 | */ |
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; |
65 | 35 |
|
66 | 36 | /** |
67 | | - * dw.json with multi-config support |
| 37 | + * Options for loading configuration. |
68 | 38 | */ |
69 | | -interface DwJsonMultiConfig extends DwJsonConfig { |
70 | | - configs?: DwJsonConfig[]; |
71 | | -} |
72 | | - |
73 | 39 | export interface LoadConfigOptions { |
| 40 | + /** Named instance from dw.json "configs" array */ |
74 | 41 | instance?: string; |
| 42 | + /** Explicit path to config file (skips searching if provided) */ |
75 | 43 | configPath?: string; |
| 44 | + /** Cloud origin for MRT ~/.mobify lookup (e.g., https://cloud-staging.mobify.com) */ |
| 45 | + cloudOrigin?: string; |
76 | 46 | } |
77 | 47 |
|
78 | 48 | /** |
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. |
172 | 55 | * |
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 |
176 | 59 | * |
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 | + * ``` |
180 | 68 | */ |
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 { |
186 | 70 | 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}`); |
215 | 83 | } |
216 | 84 |
|
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 | + } |
235 | 90 |
|
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; |
245 | 92 | } |
246 | 93 |
|
247 | 94 | /** |
|
0 commit comments