newo 1.4.0 → 1.5.1

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/dist/auth.js ADDED
@@ -0,0 +1,361 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import axios, { AxiosError } from 'axios';
4
+ import { ENV } from './env.js';
5
+ import { customerStateDir } from './fsutil.js';
6
+ const STATE_DIR = path.join(process.cwd(), '.newo');
7
+ // Constants for validation and timeouts
8
+ const API_KEY_MIN_LENGTH = 10;
9
+ const TOKEN_MIN_LENGTH = 20;
10
+ const REQUEST_TIMEOUT = 30000; // 30 seconds
11
+ const TOKEN_EXPIRY_BUFFER = 60000; // 1 minute buffer for token expiry
12
+ // Validation functions
13
+ function validateApiKey(apiKey, customerIdn) {
14
+ if (!apiKey || typeof apiKey !== 'string') {
15
+ throw new Error(`Invalid API key format${customerIdn ? ` for customer ${customerIdn}` : ''}: must be a non-empty string`);
16
+ }
17
+ if (apiKey.length < API_KEY_MIN_LENGTH) {
18
+ throw new Error(`API key too short${customerIdn ? ` for customer ${customerIdn}` : ''}: minimum ${API_KEY_MIN_LENGTH} characters required`);
19
+ }
20
+ if (apiKey.includes(' ') || apiKey.includes('\n') || apiKey.includes('\t')) {
21
+ throw new Error(`Invalid API key format${customerIdn ? ` for customer ${customerIdn}` : ''}: contains invalid characters`);
22
+ }
23
+ }
24
+ function validateTokens(tokens) {
25
+ if (!tokens.access_token || typeof tokens.access_token !== 'string' || tokens.access_token.length < TOKEN_MIN_LENGTH) {
26
+ throw new Error('Invalid access token format: must be a non-empty string with minimum length');
27
+ }
28
+ if (tokens.refresh_token && (typeof tokens.refresh_token !== 'string' || tokens.refresh_token.length < TOKEN_MIN_LENGTH)) {
29
+ throw new Error('Invalid refresh token format: must be a non-empty string with minimum length');
30
+ }
31
+ if (tokens.expires_at && (typeof tokens.expires_at !== 'number' || tokens.expires_at <= 0)) {
32
+ throw new Error('Invalid token expiry: must be a positive number');
33
+ }
34
+ }
35
+ function validateUrl(url, name) {
36
+ if (!url || typeof url !== 'string') {
37
+ throw new Error(`${name} must be a non-empty string`);
38
+ }
39
+ try {
40
+ new URL(url);
41
+ }
42
+ catch {
43
+ throw new Error(`${name} must be a valid URL format`);
44
+ }
45
+ if (!url.startsWith('https://') && !url.startsWith('http://')) {
46
+ throw new Error(`${name} must use HTTP or HTTPS protocol`);
47
+ }
48
+ }
49
+ // Enhanced logging function
50
+ function logAuthEvent(level, message, meta) {
51
+ const timestamp = new Date().toISOString();
52
+ const logEntry = {
53
+ timestamp,
54
+ level,
55
+ module: 'auth',
56
+ message,
57
+ ...meta
58
+ };
59
+ // Sanitize sensitive data
60
+ const sanitized = JSON.parse(JSON.stringify(logEntry, (key, value) => {
61
+ if (typeof key === 'string' && (key.toLowerCase().includes('key') || key.toLowerCase().includes('token') || key.toLowerCase().includes('secret'))) {
62
+ return typeof value === 'string' ? `${value.slice(0, 8)}...` : value;
63
+ }
64
+ return value;
65
+ }));
66
+ if (level === 'error') {
67
+ console.error(JSON.stringify(sanitized));
68
+ }
69
+ else if (level === 'warn') {
70
+ console.warn(JSON.stringify(sanitized));
71
+ }
72
+ else {
73
+ console.log(JSON.stringify(sanitized));
74
+ }
75
+ }
76
+ // Enhanced error handling for network requests
77
+ function handleNetworkError(error, operation, customerIdn) {
78
+ const customerInfo = customerIdn ? ` for customer ${customerIdn}` : '';
79
+ if (error instanceof AxiosError) {
80
+ const statusCode = error.response?.status;
81
+ const responseData = error.response?.data;
82
+ if (statusCode === 401) {
83
+ throw new Error(`Authentication failed${customerInfo}: Invalid API key or credentials`);
84
+ }
85
+ else if (statusCode === 403) {
86
+ throw new Error(`Access forbidden${customerInfo}: Insufficient permissions`);
87
+ }
88
+ else if (statusCode === 429) {
89
+ throw new Error(`Rate limit exceeded${customerInfo}: Please try again later`);
90
+ }
91
+ else if (statusCode && statusCode >= 500) {
92
+ throw new Error(`Server error${customerInfo}: The NEWO service is temporarily unavailable (${statusCode})`);
93
+ }
94
+ else if (error.code === 'ECONNREFUSED') {
95
+ throw new Error(`Connection refused${customerInfo}: Cannot reach NEWO service`);
96
+ }
97
+ else if (error.code === 'ETIMEDOUT' || error.code === 'ENOTFOUND') {
98
+ throw new Error(`Network timeout${customerInfo}: Check your internet connection`);
99
+ }
100
+ else {
101
+ throw new Error(`Network error during ${operation}${customerInfo}: ${error.message}${responseData ? ` - ${JSON.stringify(responseData)}` : ''}`);
102
+ }
103
+ }
104
+ throw new Error(`Failed to ${operation}${customerInfo}: ${error instanceof Error ? error.message : String(error)}`);
105
+ }
106
+ function tokensPath(customerIdn) {
107
+ if (customerIdn) {
108
+ return path.join(customerStateDir(customerIdn), 'tokens.json');
109
+ }
110
+ return path.join(STATE_DIR, 'tokens.json'); // Legacy path
111
+ }
112
+ async function saveTokens(tokens, customerIdn) {
113
+ try {
114
+ validateTokens(tokens);
115
+ const filePath = tokensPath(customerIdn);
116
+ await fs.ensureDir(path.dirname(filePath));
117
+ await fs.writeJson(filePath, tokens, { spaces: 2 });
118
+ logAuthEvent('info', 'Tokens saved successfully', {
119
+ customerIdn: customerIdn || 'legacy',
120
+ expiresAt: tokens.expires_at ? new Date(tokens.expires_at).toISOString() : undefined,
121
+ hasRefreshToken: !!tokens.refresh_token
122
+ });
123
+ }
124
+ catch (error) {
125
+ logAuthEvent('error', 'Failed to save tokens', {
126
+ customerIdn: customerIdn || 'legacy',
127
+ error: error instanceof Error ? error.message : String(error)
128
+ });
129
+ throw new Error(`Failed to save authentication tokens${customerIdn ? ` for customer ${customerIdn}` : ''}: ${error instanceof Error ? error.message : String(error)}`);
130
+ }
131
+ }
132
+ async function loadTokens(customerIdn) {
133
+ try {
134
+ const filePath = tokensPath(customerIdn);
135
+ if (await fs.pathExists(filePath)) {
136
+ const tokens = await fs.readJson(filePath);
137
+ // Validate loaded tokens
138
+ try {
139
+ validateTokens(tokens);
140
+ }
141
+ catch (validationError) {
142
+ logAuthEvent('warn', 'Loaded tokens failed validation, will regenerate', {
143
+ customerIdn: customerIdn || 'legacy',
144
+ error: validationError instanceof Error ? validationError.message : String(validationError)
145
+ });
146
+ return null; // Force token regeneration
147
+ }
148
+ logAuthEvent('info', 'Tokens loaded successfully', {
149
+ customerIdn: customerIdn || 'legacy',
150
+ expiresAt: tokens.expires_at ? new Date(tokens.expires_at).toISOString() : undefined,
151
+ hasRefreshToken: !!tokens.refresh_token
152
+ });
153
+ return tokens;
154
+ }
155
+ }
156
+ catch (error) {
157
+ logAuthEvent('warn', 'Failed to load tokens from file', {
158
+ customerIdn: customerIdn || 'legacy',
159
+ error: error instanceof Error ? error.message : String(error)
160
+ });
161
+ }
162
+ // Fallback to environment tokens for legacy mode or bootstrap
163
+ if (!customerIdn && (ENV.NEWO_ACCESS_TOKEN || ENV.NEWO_REFRESH_TOKEN)) {
164
+ const tokens = {
165
+ access_token: ENV.NEWO_ACCESS_TOKEN || '',
166
+ refresh_token: ENV.NEWO_REFRESH_TOKEN || '',
167
+ expires_at: Date.now() + 10 * 60 * 1000
168
+ };
169
+ await saveTokens(tokens);
170
+ return tokens;
171
+ }
172
+ return null;
173
+ }
174
+ function isExpired(tokens) {
175
+ if (!tokens?.expires_at) {
176
+ logAuthEvent('warn', 'Token has no expiry time, treating as expired');
177
+ return true;
178
+ }
179
+ const currentTime = Date.now();
180
+ const expiryTime = tokens.expires_at;
181
+ const timeUntilExpiry = expiryTime - currentTime;
182
+ if (timeUntilExpiry <= TOKEN_EXPIRY_BUFFER) {
183
+ logAuthEvent('info', 'Token is expired or expires soon', {
184
+ expiresAt: new Date(expiryTime).toISOString(),
185
+ timeUntilExpiry: Math.round(timeUntilExpiry / 1000)
186
+ });
187
+ return true;
188
+ }
189
+ return false;
190
+ }
191
+ function normalizeTokenResponse(tokenResponse) {
192
+ const access = tokenResponse.access_token || tokenResponse.token || tokenResponse.accessToken;
193
+ const refresh = tokenResponse.refresh_token || tokenResponse.refreshToken || '';
194
+ const expiresInSec = tokenResponse.expires_in || tokenResponse.expiresIn || 3600;
195
+ if (!access) {
196
+ throw new Error('Invalid token response: missing access token');
197
+ }
198
+ return { access, refresh, expiresInSec };
199
+ }
200
+ export async function exchangeApiKeyForToken(customer) {
201
+ const apiKey = customer?.apiKey || ENV.NEWO_API_KEY;
202
+ const customerIdn = customer?.idn;
203
+ // Validate inputs
204
+ if (!apiKey) {
205
+ throw new Error(customer
206
+ ? `API key not set for customer ${customer.idn}. Set NEWO_CUSTOMER_${customer.idn.toUpperCase()}_API_KEY in your environment`
207
+ : 'NEWO_API_KEY not set. Provide an API key in .env file');
208
+ }
209
+ validateApiKey(apiKey, customerIdn);
210
+ validateUrl(ENV.NEWO_BASE_URL, 'NEWO_BASE_URL');
211
+ logAuthEvent('info', 'Exchanging API key for tokens', { customerIdn: customerIdn || 'legacy' });
212
+ try {
213
+ const url = `${ENV.NEWO_BASE_URL}/api/v1/auth/api-key/token`;
214
+ const response = await axios.post(url, {}, {
215
+ timeout: REQUEST_TIMEOUT,
216
+ headers: {
217
+ 'x-api-key': apiKey,
218
+ 'accept': 'application/json',
219
+ 'user-agent': 'newo-cli/1.5.0'
220
+ }
221
+ });
222
+ if (!response.data) {
223
+ throw new Error('Empty response from token exchange endpoint');
224
+ }
225
+ const { access, refresh, expiresInSec } = normalizeTokenResponse(response.data);
226
+ const tokens = {
227
+ access_token: access,
228
+ refresh_token: refresh,
229
+ expires_at: Date.now() + expiresInSec * 1000
230
+ };
231
+ // Validate tokens before saving
232
+ validateTokens(tokens);
233
+ await saveTokens(tokens, customerIdn);
234
+ logAuthEvent('info', 'API key exchange completed successfully', {
235
+ customerIdn: customerIdn || 'legacy',
236
+ expiresAt: new Date(tokens.expires_at).toISOString()
237
+ });
238
+ return tokens;
239
+ }
240
+ catch (error) {
241
+ logAuthEvent('error', 'API key exchange failed', {
242
+ customerIdn: customerIdn || 'legacy',
243
+ error: error instanceof Error ? error.message : String(error)
244
+ });
245
+ handleNetworkError(error, 'exchange API key for token', customerIdn);
246
+ }
247
+ }
248
+ export async function refreshWithEndpoint(refreshToken, customer) {
249
+ const customerIdn = customer?.idn;
250
+ // Validate inputs
251
+ if (!ENV.NEWO_REFRESH_URL) {
252
+ throw new Error('NEWO_REFRESH_URL not set in environment');
253
+ }
254
+ if (!refreshToken || typeof refreshToken !== 'string' || refreshToken.length < TOKEN_MIN_LENGTH) {
255
+ throw new Error(`Invalid refresh token${customerIdn ? ` for customer ${customerIdn}` : ''}: must be a non-empty string with minimum length`);
256
+ }
257
+ validateUrl(ENV.NEWO_REFRESH_URL, 'NEWO_REFRESH_URL');
258
+ logAuthEvent('info', 'Refreshing tokens using refresh endpoint', { customerIdn: customerIdn || 'legacy' });
259
+ try {
260
+ const response = await axios.post(ENV.NEWO_REFRESH_URL, { refresh_token: refreshToken }, {
261
+ timeout: REQUEST_TIMEOUT,
262
+ headers: {
263
+ 'accept': 'application/json',
264
+ 'user-agent': 'newo-cli/1.5.0'
265
+ }
266
+ });
267
+ if (!response.data) {
268
+ throw new Error('Empty response from token refresh endpoint');
269
+ }
270
+ const { access, expiresInSec } = normalizeTokenResponse(response.data);
271
+ const refresh = response.data.refresh_token || response.data.refreshToken || refreshToken;
272
+ const tokens = {
273
+ access_token: access,
274
+ refresh_token: refresh,
275
+ expires_at: Date.now() + expiresInSec * 1000
276
+ };
277
+ // Validate tokens before saving
278
+ validateTokens(tokens);
279
+ await saveTokens(tokens, customerIdn);
280
+ logAuthEvent('info', 'Token refresh completed successfully', {
281
+ customerIdn: customerIdn || 'legacy',
282
+ expiresAt: new Date(tokens.expires_at).toISOString()
283
+ });
284
+ return tokens;
285
+ }
286
+ catch (error) {
287
+ logAuthEvent('error', 'Token refresh failed', {
288
+ customerIdn: customerIdn || 'legacy',
289
+ error: error instanceof Error ? error.message : String(error)
290
+ });
291
+ handleNetworkError(error, 'refresh token', customerIdn);
292
+ }
293
+ }
294
+ export async function getValidAccessToken(customer) {
295
+ const customerIdn = customer?.idn;
296
+ logAuthEvent('info', 'Getting valid access token', { customerIdn: customerIdn || 'legacy' });
297
+ try {
298
+ let tokens = await loadTokens(customerIdn);
299
+ // No tokens found, exchange API key
300
+ if (!tokens || !tokens.access_token) {
301
+ logAuthEvent('info', 'No existing tokens found, exchanging API key', { customerIdn: customerIdn || 'legacy' });
302
+ tokens = await exchangeApiKeyForToken(customer);
303
+ return tokens.access_token;
304
+ }
305
+ // Tokens are valid and not expired
306
+ if (!isExpired(tokens)) {
307
+ logAuthEvent('info', 'Using existing valid access token', { customerIdn: customerIdn || 'legacy' });
308
+ return tokens.access_token;
309
+ }
310
+ // Try to refresh if refresh URL and token available
311
+ if (ENV.NEWO_REFRESH_URL && tokens.refresh_token) {
312
+ try {
313
+ logAuthEvent('info', 'Attempting to refresh expired token', { customerIdn: customerIdn || 'legacy' });
314
+ tokens = await refreshWithEndpoint(tokens.refresh_token, customer);
315
+ return tokens.access_token;
316
+ }
317
+ catch (error) {
318
+ const message = error instanceof Error ? error.message : String(error);
319
+ logAuthEvent('warn', 'Token refresh failed, falling back to API key exchange', {
320
+ customerIdn: customerIdn || 'legacy',
321
+ error: message
322
+ });
323
+ }
324
+ }
325
+ else {
326
+ logAuthEvent('info', 'No refresh endpoint or refresh token available, using API key exchange', {
327
+ customerIdn: customerIdn || 'legacy'
328
+ });
329
+ }
330
+ // Fallback to API key exchange
331
+ tokens = await exchangeApiKeyForToken(customer);
332
+ return tokens.access_token;
333
+ }
334
+ catch (error) {
335
+ logAuthEvent('error', 'Failed to get valid access token', {
336
+ customerIdn: customerIdn || 'legacy',
337
+ error: error instanceof Error ? error.message : String(error)
338
+ });
339
+ throw new Error(`Unable to obtain valid access token${customerIdn ? ` for customer ${customerIdn}` : ''}: ${error instanceof Error ? error.message : String(error)}`);
340
+ }
341
+ }
342
+ export async function forceReauth(customer) {
343
+ const customerIdn = customer?.idn;
344
+ logAuthEvent('info', 'Forcing re-authentication', { customerIdn: customerIdn || 'legacy' });
345
+ try {
346
+ const tokens = await exchangeApiKeyForToken(customer);
347
+ logAuthEvent('info', 'Forced re-authentication completed successfully', {
348
+ customerIdn: customerIdn || 'legacy',
349
+ expiresAt: new Date(tokens.expires_at).toISOString()
350
+ });
351
+ return tokens.access_token;
352
+ }
353
+ catch (error) {
354
+ logAuthEvent('error', 'Forced re-authentication failed', {
355
+ customerIdn: customerIdn || 'legacy',
356
+ error: error instanceof Error ? error.message : String(error)
357
+ });
358
+ throw error;
359
+ }
360
+ }
361
+ //# sourceMappingURL=auth.js.map
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
package/dist/cli.js ADDED
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env node
2
+ import minimist from 'minimist';
3
+ import dotenv from 'dotenv';
4
+ import { makeClient, getProjectMeta, importAkbArticle } from './api.js';
5
+ import { pullAll, pushChanged, status } from './sync.js';
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';
10
+ import path from 'path';
11
+ // Enhanced error logging for CLI
12
+ function logCliError(level, message, meta) {
13
+ const timestamp = new Date().toISOString();
14
+ const logEntry = {
15
+ timestamp,
16
+ level,
17
+ module: 'cli',
18
+ message,
19
+ ...meta
20
+ };
21
+ // Only log JSON format in verbose mode, otherwise use clean user messages
22
+ const verbose = process.argv.includes('--verbose') || process.argv.includes('-v');
23
+ if (verbose) {
24
+ if (level === 'error') {
25
+ console.error(JSON.stringify(logEntry));
26
+ }
27
+ else if (level === 'warn') {
28
+ console.warn(JSON.stringify(logEntry));
29
+ }
30
+ else {
31
+ console.log(JSON.stringify(logEntry));
32
+ }
33
+ }
34
+ else {
35
+ // Clean user-facing messages
36
+ if (level === 'error') {
37
+ console.error(`❌ ${message}`);
38
+ }
39
+ else if (level === 'warn') {
40
+ console.warn(`⚠️ ${message}`);
41
+ }
42
+ else {
43
+ console.log(`ℹ️ ${message}`);
44
+ }
45
+ }
46
+ }
47
+ // Enhanced error handling with user-friendly messages
48
+ function handleCliError(error, operation = 'operation') {
49
+ const verbose = process.argv.includes('--verbose') || process.argv.includes('-v');
50
+ if (error instanceof Error) {
51
+ // Authentication errors
52
+ if (error.message.includes('API key') || error.message.includes('Authentication failed')) {
53
+ logCliError('error', 'Authentication failed. Please check your API key configuration.');
54
+ if (!verbose) {
55
+ console.error('\n💡 Troubleshooting tips:');
56
+ console.error(' • Verify your API key is correct in .env file');
57
+ console.error(' • For multi-customer setup, check NEWO_CUSTOMER_<IDN>_API_KEY');
58
+ console.error(' • Run with --verbose for detailed error information');
59
+ }
60
+ }
61
+ // Network errors
62
+ else if (error.message.includes('Network timeout') || error.message.includes('ENOTFOUND') || error.message.includes('ECONNREFUSED')) {
63
+ logCliError('error', 'Network connection failed. Please check your internet connection.');
64
+ if (!verbose) {
65
+ console.error('\n💡 Troubleshooting tips:');
66
+ console.error(' • Check your internet connection');
67
+ console.error(' • Verify NEWO_BASE_URL is correct');
68
+ console.error(' • Try again in a few moments');
69
+ }
70
+ }
71
+ // Environment configuration errors
72
+ else if (error instanceof EnvValidationError || error.message.includes('not set')) {
73
+ logCliError('error', 'Configuration error. Please check your environment setup.');
74
+ if (!verbose) {
75
+ console.error('\n💡 Setup help:');
76
+ console.error(' • Copy .env.example to .env and configure your settings');
77
+ console.error(' • Run "newo --help" to see configuration examples');
78
+ console.error(' • Check the README for detailed setup instructions');
79
+ }
80
+ }
81
+ // File system errors
82
+ else if (error.message.includes('ENOENT') || error.message.includes('EACCES')) {
83
+ logCliError('error', 'File system error. Please check file permissions and paths.');
84
+ }
85
+ // Rate limiting
86
+ else if (error.message.includes('Rate limit exceeded')) {
87
+ logCliError('error', 'Rate limit exceeded. Please wait before trying again.');
88
+ }
89
+ // General API errors
90
+ else if (error.message.includes('response') || error.message.includes('status')) {
91
+ logCliError('error', `API error during ${operation}. Please try again or contact support.`);
92
+ }
93
+ // Unknown errors
94
+ else {
95
+ logCliError('error', `Unexpected error during ${operation}: ${error.message}`);
96
+ if (!verbose) {
97
+ console.error('\n💡 For more details, run the command with --verbose flag');
98
+ }
99
+ }
100
+ if (verbose) {
101
+ logCliError('error', 'Full error details', {
102
+ operation,
103
+ errorType: error.constructor.name,
104
+ stack: error.stack?.split('\n').slice(0, 5).join('\n') // First 5 lines of stack
105
+ });
106
+ }
107
+ }
108
+ else {
109
+ logCliError('error', `Unknown error during ${operation}: ${String(error)}`);
110
+ }
111
+ process.exit(1);
112
+ }
113
+ dotenv.config();
114
+ async function main() {
115
+ try {
116
+ // Initialize and validate environment at startup
117
+ initializeEnvironment();
118
+ }
119
+ catch (error) {
120
+ if (error instanceof EnvValidationError) {
121
+ console.error('Environment validation failed:', error.message);
122
+ process.exit(1);
123
+ }
124
+ throw error;
125
+ }
126
+ const args = minimist(process.argv.slice(2));
127
+ const cmd = args._[0];
128
+ const verbose = Boolean(args.verbose || args.v);
129
+ // Parse customer configuration (async for API key array support)
130
+ let customerConfig;
131
+ try {
132
+ customerConfig = await parseCustomerConfigAsync(ENV, verbose);
133
+ validateCustomerConfig(customerConfig);
134
+ }
135
+ catch (error) {
136
+ logCliError('error', 'Failed to parse customer configuration');
137
+ if (error instanceof Error) {
138
+ logCliError('error', error.message);
139
+ }
140
+ process.exit(1);
141
+ }
142
+ // Handle customer selection
143
+ let selectedCustomer;
144
+ if (cmd === 'list-customers') {
145
+ const customers = listCustomers(customerConfig);
146
+ console.log('Available customers:');
147
+ for (const customerIdn of customers) {
148
+ const isDefault = customerConfig.defaultCustomer === customerIdn;
149
+ console.log(` ${customerIdn}${isDefault ? ' (default)' : ''}`);
150
+ }
151
+ return;
152
+ }
153
+ if (args.customer) {
154
+ const customer = getCustomer(customerConfig, args.customer);
155
+ if (!customer) {
156
+ console.error(`Unknown customer: ${args.customer}`);
157
+ console.error(`Available customers: ${listCustomers(customerConfig).join(', ')}`);
158
+ process.exit(1);
159
+ }
160
+ selectedCustomer = customer;
161
+ }
162
+ else {
163
+ try {
164
+ selectedCustomer = getDefaultCustomer(customerConfig);
165
+ }
166
+ catch (error) {
167
+ const message = error instanceof Error ? error.message : String(error);
168
+ console.error(message);
169
+ process.exit(1);
170
+ }
171
+ }
172
+ if (!cmd || ['help', '-h', '--help'].includes(cmd)) {
173
+ console.log(`NEWO CLI - Multi-Customer Support
174
+ Usage:
175
+ newo pull [--customer <idn>] # download projects -> ./newo_customers/<idn>/projects/
176
+ newo push [--customer <idn>] # upload modified *.guidance/*.jinja back to NEWO
177
+ newo status [--customer <idn>] # show modified files
178
+ newo list-customers # list available customers
179
+ newo meta [--customer <idn>] # get project metadata (debug)
180
+ newo import-akb <file> <persona_id> [--customer <idn>] # import AKB articles from file
181
+
182
+ Flags:
183
+ --customer <idn> # specify customer (if not set, uses default)
184
+ --verbose, -v # enable detailed logging
185
+
186
+ Environment Variables:
187
+ NEWO_BASE_URL # NEWO API base URL (default: https://app.newo.ai)
188
+ NEWO_CUSTOMER_<IDN>_API_KEY # API key for customer <IDN>
189
+ NEWO_CUSTOMER_<IDN>_PROJECT_ID # Optional: specific project ID for customer
190
+ NEWO_DEFAULT_CUSTOMER # Optional: default customer to use
191
+
192
+ Multi-Customer Examples:
193
+ # Configure customers in .env:
194
+ NEWO_CUSTOMER_acme_API_KEY=your_acme_api_key
195
+ NEWO_CUSTOMER_globex_API_KEY=your_globex_api_key
196
+ NEWO_DEFAULT_CUSTOMER=acme
197
+
198
+ # Commands:
199
+ newo pull --customer acme # Pull projects for Acme
200
+ newo push --customer globex # Push changes for Globex
201
+ newo status # Status for default customer
202
+
203
+ File Structure:
204
+ newo_customers/
205
+ ├── acme/
206
+ │ └── projects/
207
+ │ └── project1/
208
+ └── globex/
209
+ └── projects/
210
+ └── project2/
211
+ `);
212
+ return;
213
+ }
214
+ // Get access token for the selected customer
215
+ const accessToken = await getValidAccessToken(selectedCustomer);
216
+ const client = await makeClient(verbose, accessToken);
217
+ if (cmd === 'pull') {
218
+ // Use customer-specific project ID if set, otherwise pull all projects
219
+ const projectId = selectedCustomer.projectId || null;
220
+ await pullAll(client, selectedCustomer, projectId, verbose);
221
+ }
222
+ else if (cmd === 'push') {
223
+ await pushChanged(client, selectedCustomer, verbose);
224
+ }
225
+ else if (cmd === 'status') {
226
+ await status(selectedCustomer, verbose);
227
+ }
228
+ else if (cmd === 'meta') {
229
+ if (!selectedCustomer.projectId) {
230
+ console.error(`No project ID configured for customer ${selectedCustomer.idn}`);
231
+ console.error(`Set NEWO_CUSTOMER_${selectedCustomer.idn.toUpperCase()}_PROJECT_ID in your .env file`);
232
+ process.exit(1);
233
+ }
234
+ const meta = await getProjectMeta(client, selectedCustomer.projectId);
235
+ console.log(JSON.stringify(meta, null, 2));
236
+ }
237
+ else if (cmd === 'import-akb') {
238
+ const akbFile = args._[1];
239
+ const personaId = args._[2];
240
+ if (!akbFile || !personaId) {
241
+ console.error('Usage: newo import-akb <file> <persona_id>');
242
+ console.error('Example: newo import-akb akb.txt da4550db-2b95-4500-91ff-fb4b60fe7be9');
243
+ process.exit(1);
244
+ }
245
+ const filePath = path.resolve(akbFile);
246
+ try {
247
+ if (verbose)
248
+ console.log(`📖 Parsing AKB file: ${filePath}`);
249
+ const articles = await parseAkbFile(filePath);
250
+ console.log(`✓ Parsed ${articles.length} articles from ${akbFile}`);
251
+ if (verbose)
252
+ console.log(`🔧 Preparing articles for persona: ${personaId}`);
253
+ const preparedArticles = prepareArticlesForImport(articles, personaId);
254
+ let successCount = 0;
255
+ let errorCount = 0;
256
+ console.log(`📤 Importing ${preparedArticles.length} articles...`);
257
+ for (const [index, article] of preparedArticles.entries()) {
258
+ try {
259
+ if (verbose) {
260
+ console.log(` [${index + 1}/${preparedArticles.length}] Importing ${article.topic_name}...`);
261
+ }
262
+ await importAkbArticle(client, article);
263
+ successCount++;
264
+ if (!verbose)
265
+ process.stdout.write('.');
266
+ }
267
+ catch (error) {
268
+ errorCount++;
269
+ const errorMessage = error instanceof Error && 'response' in error
270
+ ? error?.response?.data
271
+ : error instanceof Error
272
+ ? error.message
273
+ : String(error);
274
+ console.error(`\n❌ Failed to import ${article.topic_name}:`, errorMessage);
275
+ }
276
+ }
277
+ if (!verbose)
278
+ console.log(''); // new line after dots
279
+ console.log(`✅ Import complete: ${successCount} successful, ${errorCount} failed`);
280
+ }
281
+ catch (error) {
282
+ const message = error instanceof Error ? error.message : String(error);
283
+ console.error('❌ AKB import failed:', message);
284
+ process.exit(1);
285
+ }
286
+ }
287
+ else {
288
+ console.error('Unknown command:', cmd);
289
+ process.exit(1);
290
+ }
291
+ }
292
+ main().catch((error) => {
293
+ // Determine operation context from command line args
294
+ const args = process.argv.slice(2);
295
+ const cmd = args.find(arg => !arg.startsWith('-')) || 'unknown command';
296
+ // Handle API errors with specific data
297
+ if (error instanceof Error && 'response' in error) {
298
+ const apiError = error;
299
+ const responseData = apiError.response?.data;
300
+ const status = apiError.response?.status;
301
+ if (responseData && status) {
302
+ logCliError('error', `API error (${status}): ${JSON.stringify(responseData)}`);
303
+ }
304
+ }
305
+ handleCliError(error, cmd);
306
+ });
307
+ //# sourceMappingURL=cli.js.map