newo 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.ts CHANGED
@@ -4,56 +4,239 @@ import dotenv from 'dotenv';
4
4
  import { makeClient, getProjectMeta, importAkbArticle } from './api.js';
5
5
  import { pullAll, pushChanged, status } from './sync.js';
6
6
  import { parseAkbFile, prepareArticlesForImport } from './akb.js';
7
+ import { initializeEnvironment, ENV, EnvValidationError } from './env.js';
8
+ import { parseCustomerConfigAsync, listCustomers, getCustomer, getDefaultCustomer, validateCustomerConfig } from './customerAsync.js';
9
+ import { getValidAccessToken } from './auth.js';
7
10
  import path from 'path';
8
- import type { NewoEnvironment, CliArgs, NewoApiError } from './types.js';
11
+ import type { CliArgs, NewoApiError, CustomerConfig } from './types.js';
12
+
13
+ // Enhanced error logging for CLI
14
+ function logCliError(level: 'error' | 'warn' | 'info', message: string, meta?: Record<string, unknown>): void {
15
+ const timestamp = new Date().toISOString();
16
+ const logEntry = {
17
+ timestamp,
18
+ level,
19
+ module: 'cli',
20
+ message,
21
+ ...meta
22
+ };
23
+
24
+ // Only log JSON format in verbose mode, otherwise use clean user messages
25
+ const verbose = process.argv.includes('--verbose') || process.argv.includes('-v');
26
+
27
+ if (verbose) {
28
+ if (level === 'error') {
29
+ console.error(JSON.stringify(logEntry));
30
+ } else if (level === 'warn') {
31
+ console.warn(JSON.stringify(logEntry));
32
+ } else {
33
+ console.log(JSON.stringify(logEntry));
34
+ }
35
+ } else {
36
+ // Clean user-facing messages
37
+ if (level === 'error') {
38
+ console.error(`❌ ${message}`);
39
+ } else if (level === 'warn') {
40
+ console.warn(`⚠️ ${message}`);
41
+ } else {
42
+ console.log(`ℹ️ ${message}`);
43
+ }
44
+ }
45
+ }
46
+
47
+ // Enhanced error handling with user-friendly messages
48
+ function handleCliError(error: unknown, operation: string = 'operation'): never {
49
+ const verbose = process.argv.includes('--verbose') || process.argv.includes('-v');
50
+
51
+ if (error instanceof Error) {
52
+ // Authentication errors
53
+ if (error.message.includes('API key') || error.message.includes('Authentication failed')) {
54
+ logCliError('error', 'Authentication failed. Please check your API key configuration.');
55
+ if (!verbose) {
56
+ console.error('\n💡 Troubleshooting tips:');
57
+ console.error(' • Verify your API key is correct in .env file');
58
+ console.error(' • For multi-customer setup, check NEWO_CUSTOMER_<IDN>_API_KEY');
59
+ console.error(' • Run with --verbose for detailed error information');
60
+ }
61
+ }
62
+ // Network errors
63
+ else if (error.message.includes('Network timeout') || error.message.includes('ENOTFOUND') || error.message.includes('ECONNREFUSED')) {
64
+ logCliError('error', 'Network connection failed. Please check your internet connection.');
65
+ if (!verbose) {
66
+ console.error('\n💡 Troubleshooting tips:');
67
+ console.error(' • Check your internet connection');
68
+ console.error(' • Verify NEWO_BASE_URL is correct');
69
+ console.error(' • Try again in a few moments');
70
+ }
71
+ }
72
+ // Environment configuration errors
73
+ else if (error instanceof EnvValidationError || error.message.includes('not set')) {
74
+ logCliError('error', 'Configuration error. Please check your environment setup.');
75
+ if (!verbose) {
76
+ console.error('\n💡 Setup help:');
77
+ console.error(' • Copy .env.example to .env and configure your settings');
78
+ console.error(' • Run "newo --help" to see configuration examples');
79
+ console.error(' • Check the README for detailed setup instructions');
80
+ }
81
+ }
82
+ // File system errors
83
+ else if (error.message.includes('ENOENT') || error.message.includes('EACCES')) {
84
+ logCliError('error', 'File system error. Please check file permissions and paths.');
85
+ }
86
+ // Rate limiting
87
+ else if (error.message.includes('Rate limit exceeded')) {
88
+ logCliError('error', 'Rate limit exceeded. Please wait before trying again.');
89
+ }
90
+ // General API errors
91
+ else if (error.message.includes('response') || error.message.includes('status')) {
92
+ logCliError('error', `API error during ${operation}. Please try again or contact support.`);
93
+ }
94
+ // Unknown errors
95
+ else {
96
+ logCliError('error', `Unexpected error during ${operation}: ${error.message}`);
97
+ if (!verbose) {
98
+ console.error('\n💡 For more details, run the command with --verbose flag');
99
+ }
100
+ }
101
+
102
+ if (verbose) {
103
+ logCliError('error', 'Full error details', {
104
+ operation,
105
+ errorType: error.constructor.name,
106
+ stack: error.stack?.split('\n').slice(0, 5).join('\n') // First 5 lines of stack
107
+ });
108
+ }
109
+ } else {
110
+ logCliError('error', `Unknown error during ${operation}: ${String(error)}`);
111
+ }
112
+
113
+ process.exit(1);
114
+ }
9
115
 
10
116
  dotenv.config();
11
- const { NEWO_PROJECT_ID } = process.env as NewoEnvironment;
12
117
 
13
118
  async function main(): Promise<void> {
119
+ try {
120
+ // Initialize and validate environment at startup
121
+ initializeEnvironment();
122
+ } catch (error: unknown) {
123
+ if (error instanceof EnvValidationError) {
124
+ console.error('Environment validation failed:', error.message);
125
+ process.exit(1);
126
+ }
127
+ throw error;
128
+ }
129
+
14
130
  const args = minimist(process.argv.slice(2)) as CliArgs;
15
131
  const cmd = args._[0];
16
132
  const verbose = Boolean(args.verbose || args.v);
133
+
134
+ // Parse customer configuration (async for API key array support)
135
+ let customerConfig;
136
+ try {
137
+ customerConfig = await parseCustomerConfigAsync(ENV as any, verbose);
138
+ validateCustomerConfig(customerConfig);
139
+ } catch (error: unknown) {
140
+ logCliError('error', 'Failed to parse customer configuration');
141
+ if (error instanceof Error) {
142
+ logCliError('error', error.message);
143
+ }
144
+ process.exit(1);
145
+ }
146
+
147
+ // Handle customer selection
148
+ let selectedCustomer: CustomerConfig;
149
+
150
+ if (cmd === 'list-customers') {
151
+ const customers = listCustomers(customerConfig);
152
+ console.log('Available customers:');
153
+ for (const customerIdn of customers) {
154
+ const isDefault = customerConfig.defaultCustomer === customerIdn;
155
+ console.log(` ${customerIdn}${isDefault ? ' (default)' : ''}`);
156
+ }
157
+ return;
158
+ }
159
+
160
+ if (args.customer) {
161
+ const customer = getCustomer(customerConfig, args.customer as string);
162
+ if (!customer) {
163
+ console.error(`Unknown customer: ${args.customer}`);
164
+ console.error(`Available customers: ${listCustomers(customerConfig).join(', ')}`);
165
+ process.exit(1);
166
+ }
167
+ selectedCustomer = customer;
168
+ } else {
169
+ try {
170
+ selectedCustomer = getDefaultCustomer(customerConfig);
171
+ } catch (error: unknown) {
172
+ const message = error instanceof Error ? error.message : String(error);
173
+ console.error(message);
174
+ process.exit(1);
175
+ }
176
+ }
17
177
 
18
178
  if (!cmd || ['help', '-h', '--help'].includes(cmd)) {
19
- console.log(`NEWO CLI
179
+ console.log(`NEWO CLI - Multi-Customer Support
20
180
  Usage:
21
- newo pull # download all projects -> ./projects/ OR specific project if NEWO_PROJECT_ID set
22
- newo push # upload modified *.guidance/*.jinja back to NEWO
23
- newo status # show modified files
24
- newo meta # get project metadata (debug, requires NEWO_PROJECT_ID)
25
- newo import-akb <file> <persona_id> # import AKB articles from file
181
+ newo pull [--customer <idn>] # download projects -> ./newo_customers/<idn>/projects/
182
+ newo push [--customer <idn>] # upload modified *.guidance/*.jinja back to NEWO
183
+ newo status [--customer <idn>] # show modified files
184
+ newo list-customers # list available customers
185
+ newo meta [--customer <idn>] # get project metadata (debug)
186
+ newo import-akb <file> <persona_id> [--customer <idn>] # import AKB articles from file
26
187
 
27
188
  Flags:
189
+ --customer <idn> # specify customer (if not set, uses default)
28
190
  --verbose, -v # enable detailed logging
29
191
 
30
- Env:
31
- NEWO_BASE_URL, NEWO_PROJECT_ID (optional), NEWO_API_KEY, NEWO_REFRESH_URL (optional)
192
+ Environment Variables:
193
+ NEWO_BASE_URL # NEWO API base URL (default: https://app.newo.ai)
194
+ NEWO_CUSTOMER_<IDN>_API_KEY # API key for customer <IDN>
195
+ NEWO_CUSTOMER_<IDN>_PROJECT_ID # Optional: specific project ID for customer
196
+ NEWO_DEFAULT_CUSTOMER # Optional: default customer to use
197
+
198
+ Multi-Customer Examples:
199
+ # Configure customers in .env:
200
+ NEWO_CUSTOMER_acme_API_KEY=your_acme_api_key
201
+ NEWO_CUSTOMER_globex_API_KEY=your_globex_api_key
202
+ NEWO_DEFAULT_CUSTOMER=acme
203
+
204
+ # Commands:
205
+ newo pull --customer acme # Pull projects for Acme
206
+ newo push --customer globex # Push changes for Globex
207
+ newo status # Status for default customer
32
208
 
33
- Notes:
34
- - multi-project support: pull downloads all accessible projects or single project based on NEWO_PROJECT_ID
35
- - If NEWO_PROJECT_ID is set, pull downloads only that project
36
- - If NEWO_PROJECT_ID is not set, pull downloads all projects accessible with your API key
37
- - Projects are stored in ./projects/{project-idn}/ folders
38
- - Each project folder contains metadata.json and flows.yaml
209
+ File Structure:
210
+ newo_customers/
211
+ ├── acme/
212
+ │ └── projects/
213
+ │ └── project1/
214
+ └── globex/
215
+ └── projects/
216
+ └── project2/
39
217
  `);
40
218
  return;
41
219
  }
42
220
 
43
- const client = await makeClient(verbose);
221
+ // Get access token for the selected customer
222
+ const accessToken = await getValidAccessToken(selectedCustomer);
223
+ const client = await makeClient(verbose, accessToken);
44
224
 
45
225
  if (cmd === 'pull') {
46
- // If PROJECT_ID is set, pull single project; otherwise pull all projects
47
- await pullAll(client, NEWO_PROJECT_ID || null, verbose);
226
+ // Use customer-specific project ID if set, otherwise pull all projects
227
+ const projectId = selectedCustomer.projectId || null;
228
+ await pullAll(client, selectedCustomer, projectId, verbose);
48
229
  } else if (cmd === 'push') {
49
- await pushChanged(client, verbose);
230
+ await pushChanged(client, selectedCustomer, verbose);
50
231
  } else if (cmd === 'status') {
51
- await status(verbose);
232
+ await status(selectedCustomer, verbose);
52
233
  } else if (cmd === 'meta') {
53
- if (!NEWO_PROJECT_ID) {
54
- throw new Error('NEWO_PROJECT_ID is not set in env');
234
+ if (!selectedCustomer.projectId) {
235
+ console.error(`No project ID configured for customer ${selectedCustomer.idn}`);
236
+ console.error(`Set NEWO_CUSTOMER_${selectedCustomer.idn.toUpperCase()}_PROJECT_ID in your .env file`);
237
+ process.exit(1);
55
238
  }
56
- const meta = await getProjectMeta(client, NEWO_PROJECT_ID);
239
+ const meta = await getProjectMeta(client, selectedCustomer.projectId);
57
240
  console.log(JSON.stringify(meta, null, 2));
58
241
  } else if (cmd === 'import-akb') {
59
242
  const akbFile = args._[1];
@@ -69,7 +252,7 @@ Notes:
69
252
 
70
253
  try {
71
254
  if (verbose) console.log(`📖 Parsing AKB file: ${filePath}`);
72
- const articles = parseAkbFile(filePath);
255
+ const articles = await parseAkbFile(filePath);
73
256
  console.log(`✓ Parsed ${articles.length} articles from ${akbFile}`);
74
257
 
75
258
  if (verbose) console.log(`🔧 Preparing articles for persona: ${personaId}`);
@@ -88,9 +271,13 @@ Notes:
88
271
  await importAkbArticle(client, article);
89
272
  successCount++;
90
273
  if (!verbose) process.stdout.write('.');
91
- } catch (error) {
274
+ } catch (error: unknown) {
92
275
  errorCount++;
93
- const errorMessage = (error as NewoApiError)?.response?.data || (error as Error).message;
276
+ const errorMessage = error instanceof Error && 'response' in error
277
+ ? (error as NewoApiError)?.response?.data
278
+ : error instanceof Error
279
+ ? error.message
280
+ : String(error);
94
281
  console.error(`\n❌ Failed to import ${article.topic_name}:`, errorMessage);
95
282
  }
96
283
  }
@@ -98,8 +285,9 @@ Notes:
98
285
  if (!verbose) console.log(''); // new line after dots
99
286
  console.log(`✅ Import complete: ${successCount} successful, ${errorCount} failed`);
100
287
 
101
- } catch (error) {
102
- console.error('❌ AKB import failed:', (error as Error).message);
288
+ } catch (error: unknown) {
289
+ const message = error instanceof Error ? error.message : String(error);
290
+ console.error('❌ AKB import failed:', message);
103
291
  process.exit(1);
104
292
  }
105
293
  } else {
@@ -108,8 +296,21 @@ Notes:
108
296
  }
109
297
  }
110
298
 
111
- main().catch((error: NewoApiError | Error) => {
112
- const errorData = 'response' in error ? error?.response?.data : error;
113
- console.error(errorData || error);
114
- process.exit(1);
299
+ main().catch((error: unknown) => {
300
+ // Determine operation context from command line args
301
+ const args = process.argv.slice(2);
302
+ const cmd = args.find(arg => !arg.startsWith('-')) || 'unknown command';
303
+
304
+ // Handle API errors with specific data
305
+ if (error instanceof Error && 'response' in error) {
306
+ const apiError = error as NewoApiError;
307
+ const responseData = apiError.response?.data;
308
+ const status = apiError.response?.status;
309
+
310
+ if (responseData && status) {
311
+ logCliError('error', `API error (${status}): ${JSON.stringify(responseData)}`);
312
+ }
313
+ }
314
+
315
+ handleCliError(error, cmd);
115
316
  });
@@ -0,0 +1,102 @@
1
+ import type { NewoEnvironment, CustomerConfig, MultiCustomerConfig } from './types.js';
2
+
3
+ /**
4
+ * Parse environment variables to extract customer configurations
5
+ * Supports both array-based (NEWO_API_KEYS) and individual customer configs
6
+ */
7
+ export function parseCustomerConfig(env: NewoEnvironment): MultiCustomerConfig {
8
+ const customers: Record<string, CustomerConfig> = {};
9
+
10
+ // Parse customer-specific API keys
11
+ // Format: NEWO_CUSTOMER_[IDN]_API_KEY=api_key
12
+ for (const [key, value] of Object.entries(env)) {
13
+ if (key.startsWith('NEWO_CUSTOMER_') && key.endsWith('_API_KEY') && value) {
14
+ const idn = key.slice('NEWO_CUSTOMER_'.length, -'_API_KEY'.length).toLowerCase();
15
+
16
+ if (!customers[idn]) {
17
+ customers[idn] = { idn, apiKey: value };
18
+ } else {
19
+ customers[idn].apiKey = value;
20
+ }
21
+
22
+ // Check for corresponding project ID
23
+ const projectIdKey = `NEWO_CUSTOMER_${idn.toUpperCase()}_PROJECT_ID`;
24
+ if (env[projectIdKey]) {
25
+ customers[idn].projectId = env[projectIdKey];
26
+ }
27
+ }
28
+ }
29
+
30
+ // Check for legacy single customer mode
31
+ if (env.NEWO_API_KEY && Object.keys(customers).length === 0) {
32
+ customers['default'] = {
33
+ idn: 'default',
34
+ apiKey: env.NEWO_API_KEY,
35
+ projectId: env.NEWO_PROJECT_ID
36
+ };
37
+ }
38
+
39
+ return {
40
+ customers,
41
+ defaultCustomer: env.NEWO_DEFAULT_CUSTOMER || (Object.keys(customers).length === 1 ? Object.keys(customers)[0] : undefined)
42
+ };
43
+ }
44
+
45
+ /**
46
+ * List all available customer IDNs
47
+ */
48
+ export function listCustomers(config: MultiCustomerConfig): string[] {
49
+ return Object.keys(config.customers).sort();
50
+ }
51
+
52
+ /**
53
+ * Get customer configuration by IDN
54
+ */
55
+ export function getCustomer(config: MultiCustomerConfig, customerIdn: string): CustomerConfig | null {
56
+ return config.customers[customerIdn] || null;
57
+ }
58
+
59
+ /**
60
+ * Get default customer or throw error if none
61
+ */
62
+ export function getDefaultCustomer(config: MultiCustomerConfig): CustomerConfig {
63
+ if (config.defaultCustomer) {
64
+ const customer = getCustomer(config, config.defaultCustomer);
65
+ if (customer) return customer;
66
+ }
67
+
68
+ const customerIdns = listCustomers(config);
69
+ if (customerIdns.length === 1) {
70
+ const firstCustomerIdn = customerIdns[0];
71
+ if (firstCustomerIdn) {
72
+ return config.customers[firstCustomerIdn]!;
73
+ }
74
+ }
75
+
76
+ if (customerIdns.length === 0) {
77
+ throw new Error('No customers configured. Please set NEWO_CUSTOMER_[IDN]_API_KEY in your .env file.');
78
+ }
79
+
80
+ throw new Error(
81
+ `Multiple customers configured but no default specified. Available: ${customerIdns.join(', ')}. ` +
82
+ `Set NEWO_DEFAULT_CUSTOMER or use --customer flag.`
83
+ );
84
+ }
85
+
86
+ /**
87
+ * Validate customer configuration
88
+ */
89
+ export function validateCustomerConfig(config: MultiCustomerConfig): void {
90
+ const customers = listCustomers(config);
91
+
92
+ if (customers.length === 0) {
93
+ throw new Error('No customers configured. Please set NEWO_CUSTOMER_[IDN]_API_KEY in your .env file.');
94
+ }
95
+
96
+ for (const customerIdn of customers) {
97
+ const customer = config.customers[customerIdn]!;
98
+ if (!customer.apiKey) {
99
+ throw new Error(`Customer ${customerIdn} missing API key`);
100
+ }
101
+ }
102
+ }
@@ -0,0 +1,78 @@
1
+ import type { NewoEnvironment, CustomerConfig, MultiCustomerConfig } from './types.js';
2
+ import { initializeCustomersFromApiKeys, usesArrayBasedConfig } from './customerInit.js';
3
+ import { parseCustomerConfig } from './customer.js';
4
+
5
+ /**
6
+ * Async version of customer configuration parsing that supports API key array initialization
7
+ */
8
+ export async function parseCustomerConfigAsync(env: NewoEnvironment, verbose: boolean = false): Promise<MultiCustomerConfig> {
9
+
10
+ // If using array-based config, initialize from API keys
11
+ if (usesArrayBasedConfig(env)) {
12
+ if (verbose) console.log('📝 Using array-based API key configuration');
13
+ return await initializeCustomersFromApiKeys(env, verbose);
14
+ }
15
+
16
+ // Fall back to synchronous individual customer parsing
17
+ if (verbose) console.log('📝 Using individual customer configuration');
18
+ return parseCustomerConfig(env);
19
+ }
20
+
21
+ /**
22
+ * List all available customer IDNs
23
+ */
24
+ export function listCustomers(config: MultiCustomerConfig): string[] {
25
+ return Object.keys(config.customers).sort();
26
+ }
27
+
28
+ /**
29
+ * Get customer configuration by IDN
30
+ */
31
+ export function getCustomer(config: MultiCustomerConfig, customerIdn: string): CustomerConfig | null {
32
+ return config.customers[customerIdn] || null;
33
+ }
34
+
35
+ /**
36
+ * Get default customer or throw error if none
37
+ */
38
+ export function getDefaultCustomer(config: MultiCustomerConfig): CustomerConfig {
39
+ if (config.defaultCustomer) {
40
+ const customer = getCustomer(config, config.defaultCustomer);
41
+ if (customer) return customer;
42
+ }
43
+
44
+ const customerIdns = listCustomers(config);
45
+ if (customerIdns.length === 1) {
46
+ const firstCustomerIdn = customerIdns[0];
47
+ if (firstCustomerIdn) {
48
+ return config.customers[firstCustomerIdn]!;
49
+ }
50
+ }
51
+
52
+ if (customerIdns.length === 0) {
53
+ throw new Error('No customers configured. Please set NEWO_API_KEYS or NEWO_CUSTOMER_[IDN]_API_KEY in your .env file.');
54
+ }
55
+
56
+ throw new Error(
57
+ `Multiple customers configured but no default specified. Available: ${customerIdns.join(', ')}. ` +
58
+ `Set NEWO_DEFAULT_CUSTOMER or use --customer flag.`
59
+ );
60
+ }
61
+
62
+ /**
63
+ * Validate customer configuration
64
+ */
65
+ export function validateCustomerConfig(config: MultiCustomerConfig): void {
66
+ const customers = listCustomers(config);
67
+
68
+ if (customers.length === 0) {
69
+ throw new Error('No customers configured. Please set NEWO_API_KEYS or NEWO_CUSTOMER_[IDN]_API_KEY in your .env file.');
70
+ }
71
+
72
+ for (const customerIdn of customers) {
73
+ const customer = config.customers[customerIdn]!;
74
+ if (!customer.apiKey) {
75
+ throw new Error(`Customer ${customerIdn} missing API key`);
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,97 @@
1
+ import { getCustomerProfile, makeClient } from './api.js';
2
+ import { exchangeApiKeyForToken } from './auth.js';
3
+ import type { NewoEnvironment, ApiKeyConfig, CustomerConfig, MultiCustomerConfig } from './types.js';
4
+
5
+ /**
6
+ * Initialize customer configurations from API keys array
7
+ */
8
+ export async function initializeCustomersFromApiKeys(
9
+ env: NewoEnvironment,
10
+ verbose: boolean = false
11
+ ): Promise<MultiCustomerConfig> {
12
+ if (!env.NEWO_API_KEYS) {
13
+ throw new Error('NEWO_API_KEYS not set. Provide API keys array in .env file.');
14
+ }
15
+
16
+ let apiKeyConfigs: (string | ApiKeyConfig)[];
17
+
18
+ try {
19
+ apiKeyConfigs = JSON.parse(env.NEWO_API_KEYS);
20
+ } catch (error) {
21
+ throw new Error(`Invalid NEWO_API_KEYS format. Must be valid JSON array: ${error}`);
22
+ }
23
+
24
+ if (!Array.isArray(apiKeyConfigs)) {
25
+ throw new Error('NEWO_API_KEYS must be an array');
26
+ }
27
+
28
+ const customers: Record<string, CustomerConfig> = {};
29
+
30
+ if (verbose) console.log(`🔍 Initializing ${apiKeyConfigs.length} API keys...`);
31
+
32
+ for (const [index, keyConfig] of apiKeyConfigs.entries()) {
33
+ try {
34
+ // Normalize config
35
+ const apiKey = typeof keyConfig === 'string' ? keyConfig : keyConfig.key;
36
+ const projectId = typeof keyConfig === 'object' ? keyConfig.project_id : undefined;
37
+
38
+ if (verbose) console.log(` [${index + 1}/${apiKeyConfigs.length}] Exchanging API key for token...`);
39
+
40
+ // Create temporary customer config for token exchange
41
+ const tempCustomer: CustomerConfig = {
42
+ idn: 'temp',
43
+ apiKey,
44
+ projectId
45
+ };
46
+
47
+ // Exchange API key for token
48
+ const tokens = await exchangeApiKeyForToken(tempCustomer);
49
+
50
+ if (verbose) console.log(` [${index + 1}/${apiKeyConfigs.length}] Getting customer profile...`);
51
+
52
+ // Create client with token
53
+ const client = await makeClient(verbose, tokens.access_token);
54
+
55
+ // Get customer profile to extract IDN
56
+ const profile = await getCustomerProfile(client);
57
+
58
+ if (verbose) {
59
+ console.log(` [${index + 1}/${apiKeyConfigs.length}] ✓ Customer: ${profile.idn} (${profile.organization_name})`);
60
+ }
61
+
62
+ // Store customer config with real IDN
63
+ customers[profile.idn] = {
64
+ idn: profile.idn,
65
+ apiKey,
66
+ projectId
67
+ };
68
+
69
+ } catch (error) {
70
+ const message = error instanceof Error ? error.message : String(error);
71
+ console.error(` [${index + 1}/${apiKeyConfigs.length}] ❌ Failed to initialize API key: ${message}`);
72
+ // Continue with other keys rather than failing entirely
73
+ }
74
+ }
75
+
76
+ const customerIdns = Object.keys(customers);
77
+
78
+ if (customerIdns.length === 0) {
79
+ throw new Error('No valid API keys found. Check your NEWO_API_KEYS configuration.');
80
+ }
81
+
82
+ if (verbose) {
83
+ console.log(`✅ Initialized ${customerIdns.length} customers: ${customerIdns.join(', ')}`);
84
+ }
85
+
86
+ return {
87
+ customers,
88
+ defaultCustomer: env.NEWO_DEFAULT_CUSTOMER || (customerIdns.length === 1 ? customerIdns[0] : undefined)
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Check if environment uses array-based configuration
94
+ */
95
+ export function usesArrayBasedConfig(env: NewoEnvironment): boolean {
96
+ return Boolean(env.NEWO_API_KEYS);
97
+ }