newo 3.4.0 → 3.4.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/api.d.ts +3 -1
  3. package/dist/api.js +49 -1
  4. package/dist/application/migration/MigrationEngine.d.ts +141 -0
  5. package/dist/application/migration/MigrationEngine.js +322 -0
  6. package/dist/application/migration/index.d.ts +5 -0
  7. package/dist/application/migration/index.js +5 -0
  8. package/dist/application/sync/SyncEngine.d.ts +134 -0
  9. package/dist/application/sync/SyncEngine.js +335 -0
  10. package/dist/application/sync/index.d.ts +5 -0
  11. package/dist/application/sync/index.js +5 -0
  12. package/dist/cli/commands/create-attribute.js +1 -1
  13. package/dist/cli/commands/create-customer.d.ts +3 -0
  14. package/dist/cli/commands/create-customer.js +159 -0
  15. package/dist/cli/commands/diff.d.ts +6 -0
  16. package/dist/cli/commands/diff.js +288 -0
  17. package/dist/cli/commands/help.js +63 -3
  18. package/dist/cli/commands/logs.d.ts +18 -0
  19. package/dist/cli/commands/logs.js +283 -0
  20. package/dist/cli/commands/pull.js +114 -10
  21. package/dist/cli/commands/push.js +122 -12
  22. package/dist/cli/commands/update-attribute.d.ts +3 -0
  23. package/dist/cli/commands/update-attribute.js +78 -0
  24. package/dist/cli/commands/watch.d.ts +6 -0
  25. package/dist/cli/commands/watch.js +195 -0
  26. package/dist/cli-new/bootstrap.d.ts +74 -0
  27. package/dist/cli-new/bootstrap.js +154 -0
  28. package/dist/cli-new/di/Container.d.ts +64 -0
  29. package/dist/cli-new/di/Container.js +122 -0
  30. package/dist/cli-new/di/tokens.d.ts +77 -0
  31. package/dist/cli-new/di/tokens.js +76 -0
  32. package/dist/cli.js +20 -0
  33. package/dist/domain/resources/common/types.d.ts +71 -0
  34. package/dist/domain/resources/common/types.js +42 -0
  35. package/dist/domain/strategies/sync/AkbSyncStrategy.d.ts +63 -0
  36. package/dist/domain/strategies/sync/AkbSyncStrategy.js +274 -0
  37. package/dist/domain/strategies/sync/AttributeSyncStrategy.d.ts +87 -0
  38. package/dist/domain/strategies/sync/AttributeSyncStrategy.js +378 -0
  39. package/dist/domain/strategies/sync/ConversationSyncStrategy.d.ts +61 -0
  40. package/dist/domain/strategies/sync/ConversationSyncStrategy.js +232 -0
  41. package/dist/domain/strategies/sync/ISyncStrategy.d.ts +149 -0
  42. package/dist/domain/strategies/sync/ISyncStrategy.js +24 -0
  43. package/dist/domain/strategies/sync/IntegrationSyncStrategy.d.ts +68 -0
  44. package/dist/domain/strategies/sync/IntegrationSyncStrategy.js +413 -0
  45. package/dist/domain/strategies/sync/ProjectSyncStrategy.d.ts +111 -0
  46. package/dist/domain/strategies/sync/ProjectSyncStrategy.js +523 -0
  47. package/dist/domain/strategies/sync/index.d.ts +13 -0
  48. package/dist/domain/strategies/sync/index.js +19 -0
  49. package/dist/sync/migrate.js +99 -23
  50. package/dist/types.d.ts +124 -0
  51. package/package.json +3 -1
  52. package/src/api.ts +53 -2
  53. package/src/application/migration/MigrationEngine.ts +492 -0
  54. package/src/application/migration/index.ts +5 -0
  55. package/src/application/sync/SyncEngine.ts +467 -0
  56. package/src/application/sync/index.ts +5 -0
  57. package/src/cli/commands/create-attribute.ts +1 -1
  58. package/src/cli/commands/create-customer.ts +185 -0
  59. package/src/cli/commands/diff.ts +360 -0
  60. package/src/cli/commands/help.ts +63 -3
  61. package/src/cli/commands/logs.ts +329 -0
  62. package/src/cli/commands/pull.ts +128 -11
  63. package/src/cli/commands/push.ts +131 -13
  64. package/src/cli/commands/update-attribute.ts +82 -0
  65. package/src/cli/commands/watch.ts +227 -0
  66. package/src/cli-new/bootstrap.ts +252 -0
  67. package/src/cli-new/di/Container.ts +152 -0
  68. package/src/cli-new/di/tokens.ts +105 -0
  69. package/src/cli.ts +25 -0
  70. package/src/domain/resources/common/types.ts +106 -0
  71. package/src/domain/strategies/sync/AkbSyncStrategy.ts +358 -0
  72. package/src/domain/strategies/sync/AttributeSyncStrategy.ts +508 -0
  73. package/src/domain/strategies/sync/ConversationSyncStrategy.ts +299 -0
  74. package/src/domain/strategies/sync/ISyncStrategy.ts +182 -0
  75. package/src/domain/strategies/sync/IntegrationSyncStrategy.ts +522 -0
  76. package/src/domain/strategies/sync/ProjectSyncStrategy.ts +747 -0
  77. package/src/domain/strategies/sync/index.ts +46 -0
  78. package/src/sync/migrate.ts +103 -24
  79. package/src/types.ts +135 -0
@@ -0,0 +1,467 @@
1
+ /**
2
+ * SyncEngine - Core synchronization orchestrator
3
+ *
4
+ * This is the central engine that coordinates sync operations across all resource types.
5
+ * It uses the Strategy pattern to handle different resources uniformly.
6
+ *
7
+ * Key benefits:
8
+ * - One engine handles projects, integrations, AKB, attributes, conversations
9
+ * - Adding new resource = implement one strategy class
10
+ * - No duplicate pull/push logic
11
+ * - Easy to test (mock strategies)
12
+ */
13
+
14
+ import type {
15
+ ISyncStrategy,
16
+ PullOptions,
17
+ PullResult,
18
+ PushResult,
19
+ StatusSummary,
20
+ ValidationResult
21
+ } from '../../domain/strategies/sync/ISyncStrategy.js';
22
+ import type { CustomerConfig, ILogger } from '../../domain/resources/common/types.js';
23
+
24
+ /**
25
+ * Combined pull result from all strategies
26
+ */
27
+ export interface SyncPullResult {
28
+ customer: string;
29
+ resources: Array<{
30
+ resourceType: string;
31
+ displayName: string;
32
+ result: PullResult;
33
+ }>;
34
+ totalItems: number;
35
+ errors: string[];
36
+ }
37
+
38
+ /**
39
+ * Combined push result from all strategies
40
+ */
41
+ export interface SyncPushResult {
42
+ customer: string;
43
+ resources: Array<{
44
+ resourceType: string;
45
+ displayName: string;
46
+ result: PushResult;
47
+ }>;
48
+ totalCreated: number;
49
+ totalUpdated: number;
50
+ totalDeleted: number;
51
+ errors: string[];
52
+ }
53
+
54
+ /**
55
+ * Status report for all resources
56
+ */
57
+ export interface StatusReport {
58
+ customer: string;
59
+ resources: StatusSummary[];
60
+ totalChanges: number;
61
+ }
62
+
63
+ /**
64
+ * Sync error with context
65
+ */
66
+ export class SyncError extends Error {
67
+ constructor(
68
+ message: string,
69
+ public resourceType: string,
70
+ public override cause?: Error
71
+ ) {
72
+ super(message);
73
+ this.name = 'SyncError';
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Validation error with details
79
+ */
80
+ export class ValidationError extends Error {
81
+ constructor(
82
+ message: string,
83
+ public results: ValidationResult[]
84
+ ) {
85
+ super(message);
86
+ this.name = 'ValidationError';
87
+ }
88
+ }
89
+
90
+ /**
91
+ * SyncEngine Options
92
+ */
93
+ export interface SyncEngineOptions {
94
+ /**
95
+ * Stop on first error instead of continuing
96
+ */
97
+ stopOnError?: boolean;
98
+
99
+ /**
100
+ * Run strategies in parallel where possible
101
+ */
102
+ parallel?: boolean;
103
+ }
104
+
105
+ /**
106
+ * SyncEngine - Generic synchronization orchestrator
107
+ *
108
+ * Orchestrates pull/push/status operations across all registered strategies.
109
+ */
110
+ export class SyncEngine {
111
+ private strategies: Map<string, ISyncStrategy> = new Map();
112
+
113
+ constructor(
114
+ strategies: ISyncStrategy[],
115
+ private logger: ILogger,
116
+ private options: SyncEngineOptions = {}
117
+ ) {
118
+ for (const strategy of strategies) {
119
+ this.strategies.set(strategy.resourceType, strategy);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Register a new strategy
125
+ */
126
+ registerStrategy(strategy: ISyncStrategy): void {
127
+ this.strategies.set(strategy.resourceType, strategy);
128
+ }
129
+
130
+ /**
131
+ * Get a specific strategy by resource type
132
+ */
133
+ getStrategy(resourceType: string): ISyncStrategy | undefined {
134
+ return this.strategies.get(resourceType);
135
+ }
136
+
137
+ /**
138
+ * Get all registered strategies
139
+ */
140
+ getStrategies(): ISyncStrategy[] {
141
+ return Array.from(this.strategies.values());
142
+ }
143
+
144
+ /**
145
+ * Pull ALL resources using registered strategies
146
+ */
147
+ async pullAll(customer: CustomerConfig, options: PullOptions = {}): Promise<SyncPullResult> {
148
+ this.logger.info(`📥 Pulling all resources for customer: ${customer.idn}`);
149
+
150
+ const result: SyncPullResult = {
151
+ customer: customer.idn,
152
+ resources: [],
153
+ totalItems: 0,
154
+ errors: []
155
+ };
156
+
157
+ const strategies = Array.from(this.strategies.values());
158
+
159
+ if (this.options.parallel) {
160
+ // Parallel execution
161
+ const pullPromises = strategies.map(async (strategy) => {
162
+ try {
163
+ return await this.pullWithStrategy(strategy, customer, options);
164
+ } catch (error) {
165
+ const message = `Failed to pull ${strategy.displayName}: ${error instanceof Error ? error.message : String(error)}`;
166
+ if (this.options.stopOnError) {
167
+ throw new SyncError(message, strategy.resourceType, error instanceof Error ? error : undefined);
168
+ }
169
+ result.errors.push(message);
170
+ return null;
171
+ }
172
+ });
173
+
174
+ const pullResults = await Promise.all(pullPromises);
175
+
176
+ for (const pullResult of pullResults) {
177
+ if (pullResult) {
178
+ result.resources.push(pullResult);
179
+ result.totalItems += pullResult.result.count;
180
+ }
181
+ }
182
+ } else {
183
+ // Sequential execution
184
+ for (const strategy of strategies) {
185
+ this.logger.info(` 📦 Pulling ${strategy.displayName}...`);
186
+
187
+ try {
188
+ const pullResult = await this.pullWithStrategy(strategy, customer, options);
189
+ result.resources.push(pullResult);
190
+ result.totalItems += pullResult.result.count;
191
+ this.logger.info(` ✅ Pulled ${pullResult.result.count} ${strategy.displayName}`);
192
+ } catch (error) {
193
+ const message = `Failed to pull ${strategy.displayName}: ${error instanceof Error ? error.message : String(error)}`;
194
+ this.logger.error(message, error);
195
+
196
+ if (this.options.stopOnError) {
197
+ throw new SyncError(message, strategy.resourceType, error instanceof Error ? error : undefined);
198
+ }
199
+ result.errors.push(message);
200
+ }
201
+ }
202
+ }
203
+
204
+ this.logger.info(`✅ Pull completed: ${result.totalItems} items from ${result.resources.length} resource types`);
205
+
206
+ return result;
207
+ }
208
+
209
+ /**
210
+ * Pull specific resource types
211
+ */
212
+ async pullSelected(
213
+ customer: CustomerConfig,
214
+ resourceTypes: string[],
215
+ options: PullOptions = {}
216
+ ): Promise<SyncPullResult> {
217
+ this.logger.info(`📥 Pulling selected resources for customer: ${customer.idn}`);
218
+
219
+ const result: SyncPullResult = {
220
+ customer: customer.idn,
221
+ resources: [],
222
+ totalItems: 0,
223
+ errors: []
224
+ };
225
+
226
+ for (const resourceType of resourceTypes) {
227
+ const strategy = this.strategies.get(resourceType);
228
+
229
+ if (!strategy) {
230
+ result.errors.push(`Unknown resource type: ${resourceType}`);
231
+ continue;
232
+ }
233
+
234
+ this.logger.info(` 📦 Pulling ${strategy.displayName}...`);
235
+
236
+ try {
237
+ const pullResult = await this.pullWithStrategy(strategy, customer, options);
238
+ result.resources.push(pullResult);
239
+ result.totalItems += pullResult.result.count;
240
+ this.logger.info(` ✅ Pulled ${pullResult.result.count} ${strategy.displayName}`);
241
+ } catch (error) {
242
+ const message = `Failed to pull ${strategy.displayName}: ${error instanceof Error ? error.message : String(error)}`;
243
+ this.logger.error(message, error);
244
+
245
+ if (this.options.stopOnError) {
246
+ throw new SyncError(message, strategy.resourceType, error instanceof Error ? error : undefined);
247
+ }
248
+ result.errors.push(message);
249
+ }
250
+ }
251
+
252
+ return result;
253
+ }
254
+
255
+ /**
256
+ * Push ALL changed resources using registered strategies
257
+ */
258
+ async pushAll(customer: CustomerConfig): Promise<SyncPushResult> {
259
+ this.logger.info(`📤 Pushing changes for customer: ${customer.idn}`);
260
+
261
+ const result: SyncPushResult = {
262
+ customer: customer.idn,
263
+ resources: [],
264
+ totalCreated: 0,
265
+ totalUpdated: 0,
266
+ totalDeleted: 0,
267
+ errors: []
268
+ };
269
+
270
+ for (const strategy of this.strategies.values()) {
271
+ this.logger.info(` 🔍 Checking changes for ${strategy.displayName}...`);
272
+
273
+ try {
274
+ const changes = await strategy.getChanges(customer);
275
+
276
+ if (changes.length === 0) {
277
+ this.logger.verbose(` No changes for ${strategy.displayName}`);
278
+ continue;
279
+ }
280
+
281
+ this.logger.info(` Found ${changes.length} changes in ${strategy.displayName}`);
282
+
283
+ // Validate before push
284
+ const items = changes.map(c => c.item);
285
+ const validation = await strategy.validate(customer, items);
286
+
287
+ if (!validation.valid) {
288
+ const errorMessages = validation.errors.map(e => `${e.field}: ${e.message}`).join(', ');
289
+ throw new ValidationError(`Validation failed: ${errorMessages}`, [validation]);
290
+ }
291
+
292
+ // Push changes
293
+ const pushResult = await strategy.push(customer, changes);
294
+
295
+ result.resources.push({
296
+ resourceType: strategy.resourceType,
297
+ displayName: strategy.displayName,
298
+ result: pushResult
299
+ });
300
+
301
+ result.totalCreated += pushResult.created;
302
+ result.totalUpdated += pushResult.updated;
303
+ result.totalDeleted += pushResult.deleted;
304
+ result.errors.push(...pushResult.errors);
305
+
306
+ this.logger.info(` ✅ Pushed: ${pushResult.created} created, ${pushResult.updated} updated, ${pushResult.deleted} deleted`);
307
+ } catch (error) {
308
+ const message = `Failed to push ${strategy.displayName}: ${error instanceof Error ? error.message : String(error)}`;
309
+ this.logger.error(message, error);
310
+
311
+ if (this.options.stopOnError) {
312
+ throw new SyncError(message, strategy.resourceType, error instanceof Error ? error : undefined);
313
+ }
314
+ result.errors.push(message);
315
+ }
316
+ }
317
+
318
+ this.logger.info(`✅ Push completed: ${result.totalCreated} created, ${result.totalUpdated} updated, ${result.totalDeleted} deleted`);
319
+
320
+ return result;
321
+ }
322
+
323
+ /**
324
+ * Push specific resource types
325
+ */
326
+ async pushSelected(customer: CustomerConfig, resourceTypes: string[]): Promise<SyncPushResult> {
327
+ this.logger.info(`📤 Pushing selected resources for customer: ${customer.idn}`);
328
+
329
+ const result: SyncPushResult = {
330
+ customer: customer.idn,
331
+ resources: [],
332
+ totalCreated: 0,
333
+ totalUpdated: 0,
334
+ totalDeleted: 0,
335
+ errors: []
336
+ };
337
+
338
+ for (const resourceType of resourceTypes) {
339
+ const strategy = this.strategies.get(resourceType);
340
+
341
+ if (!strategy) {
342
+ result.errors.push(`Unknown resource type: ${resourceType}`);
343
+ continue;
344
+ }
345
+
346
+ try {
347
+ const changes = await strategy.getChanges(customer);
348
+
349
+ if (changes.length === 0) {
350
+ continue;
351
+ }
352
+
353
+ // Validate before push
354
+ const items = changes.map(c => c.item);
355
+ const validation = await strategy.validate(customer, items);
356
+
357
+ if (!validation.valid) {
358
+ const errorMessages = validation.errors.map(e => `${e.field}: ${e.message}`).join(', ');
359
+ throw new ValidationError(`Validation failed: ${errorMessages}`, [validation]);
360
+ }
361
+
362
+ const pushResult = await strategy.push(customer, changes);
363
+
364
+ result.resources.push({
365
+ resourceType: strategy.resourceType,
366
+ displayName: strategy.displayName,
367
+ result: pushResult
368
+ });
369
+
370
+ result.totalCreated += pushResult.created;
371
+ result.totalUpdated += pushResult.updated;
372
+ result.totalDeleted += pushResult.deleted;
373
+ result.errors.push(...pushResult.errors);
374
+ } catch (error) {
375
+ const message = `Failed to push ${strategy.displayName}: ${error instanceof Error ? error.message : String(error)}`;
376
+ this.logger.error(message, error);
377
+
378
+ if (this.options.stopOnError) {
379
+ throw new SyncError(message, strategy.resourceType, error instanceof Error ? error : undefined);
380
+ }
381
+ result.errors.push(message);
382
+ }
383
+ }
384
+
385
+ return result;
386
+ }
387
+
388
+ /**
389
+ * Get status for ALL resources
390
+ */
391
+ async getStatus(customer: CustomerConfig): Promise<StatusReport> {
392
+ const report: StatusReport = {
393
+ customer: customer.idn,
394
+ resources: [],
395
+ totalChanges: 0
396
+ };
397
+
398
+ for (const strategy of this.strategies.values()) {
399
+ try {
400
+ const status = await strategy.getStatus(customer);
401
+ report.resources.push(status);
402
+ report.totalChanges += status.changedCount;
403
+ } catch (error) {
404
+ this.logger.error(`Failed to get status for ${strategy.displayName}`, error);
405
+ }
406
+ }
407
+
408
+ return report;
409
+ }
410
+
411
+ /**
412
+ * Get status for specific resource types
413
+ */
414
+ async getStatusSelected(customer: CustomerConfig, resourceTypes: string[]): Promise<StatusReport> {
415
+ const report: StatusReport = {
416
+ customer: customer.idn,
417
+ resources: [],
418
+ totalChanges: 0
419
+ };
420
+
421
+ for (const resourceType of resourceTypes) {
422
+ const strategy = this.strategies.get(resourceType);
423
+
424
+ if (!strategy) {
425
+ continue;
426
+ }
427
+
428
+ try {
429
+ const status = await strategy.getStatus(customer);
430
+ report.resources.push(status);
431
+ report.totalChanges += status.changedCount;
432
+ } catch (error) {
433
+ this.logger.error(`Failed to get status for ${strategy.displayName}`, error);
434
+ }
435
+ }
436
+
437
+ return report;
438
+ }
439
+
440
+ /**
441
+ * Helper to execute pull with a single strategy
442
+ */
443
+ private async pullWithStrategy(
444
+ strategy: ISyncStrategy,
445
+ customer: CustomerConfig,
446
+ options: PullOptions
447
+ ): Promise<{ resourceType: string; displayName: string; result: PullResult }> {
448
+ const result = await strategy.pull(customer, options);
449
+
450
+ return {
451
+ resourceType: strategy.resourceType,
452
+ displayName: strategy.displayName,
453
+ result
454
+ };
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Factory function for creating SyncEngine with default strategies
460
+ */
461
+ export function createSyncEngine(
462
+ strategies: ISyncStrategy[],
463
+ logger: ILogger,
464
+ options?: SyncEngineOptions
465
+ ): SyncEngine {
466
+ return new SyncEngine(strategies, logger, options);
467
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Sync Application Layer Exports
3
+ */
4
+
5
+ export * from './SyncEngine.js';
@@ -16,7 +16,7 @@ export async function handleCreateAttributeCommand(
16
16
 
17
17
  // Parse arguments
18
18
  const idn = args._[1] as string;
19
- const value = args.value as string || '';
19
+ const value = args.value !== undefined ? String(args.value) : '';
20
20
  const title = args.title as string || idn;
21
21
  const description = args.description as string || '';
22
22
  const group = args.group as string || 'General';
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Create Customer Command Handler - Creates a new NEWO customer account
3
+ *
4
+ * This command creates an empty NEWO customer using the v3 API.
5
+ * It requires the api_secret attribute from the source customer account.
6
+ */
7
+ import { makeClient, createNewoCustomer, getCustomerAttributes } from '../../api.js';
8
+ import { getValidAccessToken } from '../../auth.js';
9
+ import { requireSingleCustomer } from '../customer-selection.js';
10
+ import type {
11
+ MultiCustomerConfig,
12
+ CliArgs,
13
+ CreateNewoCustomerRequest,
14
+ CustomerMember,
15
+ CustomerProjectInput
16
+ } from '../../types.js';
17
+
18
+ export async function handleCreateCustomerCommand(
19
+ customerConfig: MultiCustomerConfig,
20
+ args: CliArgs,
21
+ verbose: boolean = false
22
+ ): Promise<void> {
23
+ try {
24
+ const selectedCustomer = requireSingleCustomer(customerConfig, args.customer as string | undefined);
25
+
26
+ // Parse arguments
27
+ const organizationName = args._[1] as string;
28
+ const email = args.email as string;
29
+ const tenant = (args.tenant as string) || 'newo';
30
+ const phone = (args.phone as string) || '';
31
+ const comment = (args.comment as string) || '';
32
+ const status = (args.status as string) || 'temporal';
33
+ const projectIdn = args.project as string | undefined;
34
+ const externalId = args['external-id'] as string | undefined;
35
+
36
+ // Validate required parameters
37
+ if (!organizationName) {
38
+ console.error('Error: Organization name is required');
39
+ console.error('Usage: newo create-customer <organization_name> --email <email> [options]');
40
+ console.error('');
41
+ console.error('Options:');
42
+ console.error(' --email <email> Owner email (required)');
43
+ console.error(' --tenant <tenant> Tenant name (default: newo)');
44
+ console.error(' --phone <phone> Contact phone number');
45
+ console.error(' --comment <comment> Comment or notes');
46
+ console.error(' --status <status> temporal or permanent (default: temporal)');
47
+ console.error(' --project <idn> Project IDN to install (e.g., naf)');
48
+ console.error(' --external-id <id> External customer ID for tracking');
49
+ console.error('');
50
+ console.error('Example:');
51
+ console.error(' newo create-customer "Acme Corp" --email owner@acme.com --project naf');
52
+ process.exit(1);
53
+ }
54
+
55
+ if (!email) {
56
+ console.error('Error: Owner email is required (--email <email>)');
57
+ process.exit(1);
58
+ }
59
+
60
+ if (verbose) {
61
+ console.log(`📝 Creating new NEWO customer...`);
62
+ console.log(` Organization: ${organizationName}`);
63
+ console.log(` Owner Email: ${email}`);
64
+ console.log(` Tenant: ${tenant}`);
65
+ console.log(` Status: ${status}`);
66
+ if (phone) console.log(` Phone: ${phone}`);
67
+ if (comment) console.log(` Comment: ${comment}`);
68
+ if (projectIdn) console.log(` Project: ${projectIdn}`);
69
+ if (externalId) console.log(` External ID: ${externalId}`);
70
+ }
71
+
72
+ // Get access token and create client
73
+ const accessToken = await getValidAccessToken(selectedCustomer);
74
+ const client = await makeClient(verbose, accessToken);
75
+
76
+ // Get api_secret from customer attributes
77
+ console.log('🔑 Fetching API secret from customer attributes...');
78
+ const attributesResponse = await getCustomerAttributes(client, true);
79
+ const apiSecretAttr = attributesResponse.attributes.find(attr => attr.idn === 'api_secret');
80
+
81
+ if (!apiSecretAttr || !apiSecretAttr.value) {
82
+ console.error('Error: api_secret attribute not found in customer account');
83
+ console.error('This command requires the api_secret attribute to create new customers.');
84
+ process.exit(1);
85
+ }
86
+
87
+ const apiSecret = typeof apiSecretAttr.value === 'string' ? apiSecretAttr.value : String(apiSecretAttr.value);
88
+
89
+ if (verbose) {
90
+ console.log(`✅ API secret found (${apiSecret.substring(0, 6)}...)`);
91
+ }
92
+
93
+ // Build members array
94
+ const members: CustomerMember[] = [
95
+ {
96
+ email: email,
97
+ role: 'owner',
98
+ tenants: [tenant]
99
+ }
100
+ ];
101
+
102
+ // Build projects array if project specified
103
+ const projects: CustomerProjectInput[] = [];
104
+ if (projectIdn) {
105
+ projects.push({
106
+ idn: projectIdn
107
+ });
108
+ }
109
+
110
+ // Build customer object - only include optional fields if they have values
111
+ const customerData: CreateNewoCustomerRequest['customer'] = {
112
+ organization_name: organizationName,
113
+ tenant: tenant,
114
+ members: members,
115
+ contact_email: email,
116
+ organization_type: 'customer',
117
+ organization_status: status as 'temporal' | 'permanent',
118
+ attributes: [
119
+ {
120
+ idn: 'empty',
121
+ value: 'True'
122
+ }
123
+ ]
124
+ };
125
+
126
+ // Add optional fields only if they have values
127
+ if (comment) {
128
+ customerData.comment = comment;
129
+ }
130
+ if (phone) {
131
+ customerData.contact_phone = phone;
132
+ }
133
+ if (externalId) {
134
+ customerData.external_customer_id = externalId;
135
+ }
136
+
137
+ // Build customer creation request
138
+ const createRequest: CreateNewoCustomerRequest = {
139
+ secret: apiSecret,
140
+ customer: customerData
141
+ };
142
+
143
+ // Add projects only if specified
144
+ if (projects.length > 0) {
145
+ createRequest.projects = projects;
146
+ }
147
+
148
+ if (verbose) {
149
+ console.log('📤 Creating customer with request:');
150
+ console.log(JSON.stringify(createRequest, null, 2));
151
+ }
152
+
153
+ // Create the customer
154
+ console.log('🚀 Creating customer...');
155
+ const response = await createNewoCustomer(client, createRequest);
156
+
157
+ console.log('');
158
+ console.log('✅ Customer created successfully!');
159
+ console.log(` Customer IDN: ${response.idn}`);
160
+ console.log(` Customer ID: ${response.id}`);
161
+ console.log(` Organization: ${organizationName}`);
162
+ console.log(` Owner: ${email}`);
163
+ if (projectIdn) {
164
+ console.log(` Project: ${projectIdn}`);
165
+ }
166
+ console.log('');
167
+ console.log('📝 Next steps:');
168
+ console.log(` 1. Add the new customer to your .env file:`);
169
+ console.log(` NEWO_${response.idn}_API_KEY=<api_key>`);
170
+ console.log(` 2. Or use the customer_intercom integration to manage the new customer`);
171
+
172
+ } catch (error: unknown) {
173
+ const errMessage = error instanceof Error ? error.message : String(error);
174
+ console.error('❌ Failed to create customer:', errMessage);
175
+
176
+ // Provide more detailed error info if available
177
+ if (error && typeof error === 'object' && 'response' in error) {
178
+ const axiosError = error as any;
179
+ if (axiosError.response?.data) {
180
+ console.error(' API Error:', JSON.stringify(axiosError.response.data, null, 2));
181
+ }
182
+ }
183
+ process.exit(1);
184
+ }
185
+ }