newo 3.3.3 → 3.4.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/api.d.ts +6 -1
  3. package/dist/api.js +63 -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/add-project.d.ts +3 -0
  13. package/dist/cli/commands/add-project.js +136 -0
  14. package/dist/cli/commands/create-customer.d.ts +3 -0
  15. package/dist/cli/commands/create-customer.js +159 -0
  16. package/dist/cli/commands/diff.d.ts +6 -0
  17. package/dist/cli/commands/diff.js +288 -0
  18. package/dist/cli/commands/help.js +75 -4
  19. package/dist/cli/commands/list-registries.d.ts +3 -0
  20. package/dist/cli/commands/list-registries.js +39 -0
  21. package/dist/cli/commands/list-registry-items.d.ts +3 -0
  22. package/dist/cli/commands/list-registry-items.js +112 -0
  23. package/dist/cli/commands/logs.d.ts +18 -0
  24. package/dist/cli/commands/logs.js +283 -0
  25. package/dist/cli/commands/pull.js +114 -10
  26. package/dist/cli/commands/push.js +122 -12
  27. package/dist/cli/commands/watch.d.ts +6 -0
  28. package/dist/cli/commands/watch.js +195 -0
  29. package/dist/cli-new/bootstrap.d.ts +74 -0
  30. package/dist/cli-new/bootstrap.js +154 -0
  31. package/dist/cli-new/di/Container.d.ts +64 -0
  32. package/dist/cli-new/di/Container.js +122 -0
  33. package/dist/cli-new/di/tokens.d.ts +77 -0
  34. package/dist/cli-new/di/tokens.js +76 -0
  35. package/dist/cli.js +28 -0
  36. package/dist/domain/resources/common/types.d.ts +71 -0
  37. package/dist/domain/resources/common/types.js +42 -0
  38. package/dist/domain/strategies/sync/AkbSyncStrategy.d.ts +63 -0
  39. package/dist/domain/strategies/sync/AkbSyncStrategy.js +274 -0
  40. package/dist/domain/strategies/sync/AttributeSyncStrategy.d.ts +87 -0
  41. package/dist/domain/strategies/sync/AttributeSyncStrategy.js +378 -0
  42. package/dist/domain/strategies/sync/ConversationSyncStrategy.d.ts +61 -0
  43. package/dist/domain/strategies/sync/ConversationSyncStrategy.js +232 -0
  44. package/dist/domain/strategies/sync/ISyncStrategy.d.ts +149 -0
  45. package/dist/domain/strategies/sync/ISyncStrategy.js +24 -0
  46. package/dist/domain/strategies/sync/IntegrationSyncStrategy.d.ts +68 -0
  47. package/dist/domain/strategies/sync/IntegrationSyncStrategy.js +413 -0
  48. package/dist/domain/strategies/sync/ProjectSyncStrategy.d.ts +111 -0
  49. package/dist/domain/strategies/sync/ProjectSyncStrategy.js +523 -0
  50. package/dist/domain/strategies/sync/index.d.ts +13 -0
  51. package/dist/domain/strategies/sync/index.js +19 -0
  52. package/dist/sync/migrate.js +99 -23
  53. package/dist/types.d.ts +162 -0
  54. package/package.json +3 -1
  55. package/src/api.ts +77 -2
  56. package/src/application/migration/MigrationEngine.ts +492 -0
  57. package/src/application/migration/index.ts +5 -0
  58. package/src/application/sync/SyncEngine.ts +467 -0
  59. package/src/application/sync/index.ts +5 -0
  60. package/src/cli/commands/add-project.ts +159 -0
  61. package/src/cli/commands/create-customer.ts +185 -0
  62. package/src/cli/commands/diff.ts +360 -0
  63. package/src/cli/commands/help.ts +75 -4
  64. package/src/cli/commands/list-registries.ts +53 -0
  65. package/src/cli/commands/list-registry-items.ts +149 -0
  66. package/src/cli/commands/logs.ts +329 -0
  67. package/src/cli/commands/pull.ts +128 -11
  68. package/src/cli/commands/push.ts +131 -13
  69. package/src/cli/commands/watch.ts +227 -0
  70. package/src/cli-new/bootstrap.ts +252 -0
  71. package/src/cli-new/di/Container.ts +152 -0
  72. package/src/cli-new/di/tokens.ts +105 -0
  73. package/src/cli.ts +35 -0
  74. package/src/domain/resources/common/types.ts +106 -0
  75. package/src/domain/strategies/sync/AkbSyncStrategy.ts +358 -0
  76. package/src/domain/strategies/sync/AttributeSyncStrategy.ts +508 -0
  77. package/src/domain/strategies/sync/ConversationSyncStrategy.ts +299 -0
  78. package/src/domain/strategies/sync/ISyncStrategy.ts +182 -0
  79. package/src/domain/strategies/sync/IntegrationSyncStrategy.ts +522 -0
  80. package/src/domain/strategies/sync/ProjectSyncStrategy.ts +747 -0
  81. package/src/domain/strategies/sync/index.ts +46 -0
  82. package/src/sync/migrate.ts +103 -24
  83. package/src/types.ts +178 -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';
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Add Project from Registry Command Handler - Installs a project template from registry
3
+ */
4
+ import { makeClient, listRegistries, listRegistryItems, addProjectFromRegistry } from '../../api.js';
5
+ import { getValidAccessToken } from '../../auth.js';
6
+ import { requireSingleCustomer } from '../customer-selection.js';
7
+ import type { MultiCustomerConfig, CliArgs, Registry, RegistryItem, AddProjectFromRegistryRequest } from '../../types.js';
8
+
9
+ export async function handleAddProjectCommand(
10
+ customerConfig: MultiCustomerConfig,
11
+ args: CliArgs,
12
+ verbose: boolean = false
13
+ ): Promise<void> {
14
+ try {
15
+ const selectedCustomer = requireSingleCustomer(customerConfig, args.customer as string | undefined);
16
+
17
+ // Parse arguments
18
+ const projectIdn = args._[1] as string;
19
+ const registryIdn = args.registry as string || 'production';
20
+ const registryItemIdn = args.item as string;
21
+ const registryItemVersion = args.version as string | null || null;
22
+ const title = args.title as string || projectIdn || registryItemIdn;
23
+ const description = args.description as string || '';
24
+ const isAutoUpdateEnabled = Boolean(args['auto-update']);
25
+
26
+ // Validate required arguments
27
+ if (!registryItemIdn) {
28
+ console.error('Error: Registry item IDN is required');
29
+ console.error('');
30
+ console.error('Usage: newo add-project <project-idn> --item <registry-item-idn> [options]');
31
+ console.error('');
32
+ console.error('Options:');
33
+ console.error(' --item <idn> Registry item/template IDN (required)');
34
+ console.error(' --registry <idn> Registry to use (default: production)');
35
+ console.error(' --version <version> Specific version to install (default: latest)');
36
+ console.error(' --title <title> Project title (default: project IDN)');
37
+ console.error(' --description <desc> Project description');
38
+ console.error(' --auto-update Enable automatic updates from registry');
39
+ console.error('');
40
+ console.error('Examples:');
41
+ console.error(' newo add-project my_weather --item weather_integration');
42
+ console.error(' newo add-project my_calcom --item cal_com_integration --registry production');
43
+ console.error(' newo add-project my_zoho --item zoho_integration --version 1.0.2 --auto-update');
44
+ console.error('');
45
+ console.error('Run "newo list-registries" to see available registries');
46
+ console.error('Run "newo list-registry-items <registry-idn>" to see available project templates');
47
+ process.exit(1);
48
+ }
49
+
50
+ // Use registry item IDN as project IDN if not specified
51
+ const finalProjectIdn = projectIdn || registryItemIdn;
52
+
53
+ if (verbose) {
54
+ console.log(`📦 Adding project from registry`);
55
+ console.log(` Project IDN: ${finalProjectIdn}`);
56
+ console.log(` Title: ${title}`);
57
+ console.log(` Registry: ${registryIdn}`);
58
+ console.log(` Item: ${registryItemIdn}`);
59
+ console.log(` Version: ${registryItemVersion || 'latest'}`);
60
+ console.log(` Auto-update: ${isAutoUpdateEnabled}`);
61
+ console.log(` Customer: ${selectedCustomer.idn}`);
62
+ }
63
+
64
+ // Get access token and create client
65
+ const accessToken = await getValidAccessToken(selectedCustomer);
66
+ const client = await makeClient(verbose, accessToken);
67
+
68
+ // Validate registry exists
69
+ console.log(`🔍 Validating registry "${registryIdn}"...`);
70
+ const registries = await listRegistries(client);
71
+ const registry = registries.find((r: Registry) => r.idn === registryIdn);
72
+
73
+ if (!registry) {
74
+ console.error(`❌ Registry "${registryIdn}" not found`);
75
+ console.error('');
76
+ console.error('Available registries:');
77
+ for (const r of registries) {
78
+ console.error(` • ${r.idn}`);
79
+ }
80
+ process.exit(1);
81
+ }
82
+
83
+ // Validate registry item exists and find version
84
+ console.log(`🔍 Validating project template "${registryItemIdn}"...`);
85
+ const items = await listRegistryItems(client, registry.id);
86
+ const matchingItems = items.filter((item: RegistryItem) => item.idn === registryItemIdn);
87
+
88
+ if (matchingItems.length === 0) {
89
+ console.error(`❌ Project template "${registryItemIdn}" not found in "${registryIdn}" registry`);
90
+ console.error('');
91
+ console.error('Run "newo list-registry-items ' + registryIdn + '" to see available templates');
92
+ process.exit(1);
93
+ }
94
+
95
+ // Find the specific version or latest
96
+ let selectedItem: RegistryItem | undefined;
97
+ if (registryItemVersion) {
98
+ selectedItem = matchingItems.find((item: RegistryItem) => item.version === registryItemVersion);
99
+ if (!selectedItem) {
100
+ console.error(`❌ Version "${registryItemVersion}" not found for "${registryItemIdn}"`);
101
+ console.error('');
102
+ console.error('Available versions:');
103
+ const sortedItems = [...matchingItems].sort((a, b) =>
104
+ new Date(b.published_at).getTime() - new Date(a.published_at).getTime()
105
+ );
106
+ for (const item of sortedItems.slice(0, 10)) {
107
+ console.error(` • ${item.version} (published: ${new Date(item.published_at).toISOString().split('T')[0]})`);
108
+ }
109
+ if (sortedItems.length > 10) {
110
+ console.error(` ... and ${sortedItems.length - 10} more`);
111
+ }
112
+ process.exit(1);
113
+ }
114
+ } else {
115
+ // Get latest version (sorted by published_at desc)
116
+ const sortedItems = [...matchingItems].sort((a, b) =>
117
+ new Date(b.published_at).getTime() - new Date(a.published_at).getTime()
118
+ );
119
+ selectedItem = sortedItems[0];
120
+ }
121
+
122
+ if (!selectedItem) {
123
+ console.error(`❌ Could not determine version for "${registryItemIdn}"`);
124
+ process.exit(1);
125
+ }
126
+
127
+ console.log(`📥 Installing "${registryItemIdn}" v${selectedItem.version} as "${finalProjectIdn}"...`);
128
+
129
+ // Create project from registry
130
+ const projectData: AddProjectFromRegistryRequest = {
131
+ idn: finalProjectIdn,
132
+ title,
133
+ version: '',
134
+ description,
135
+ is_auto_update_enabled: isAutoUpdateEnabled,
136
+ registry_idn: registryIdn,
137
+ registry_item_idn: registryItemIdn,
138
+ registry_item_version: registryItemVersion
139
+ };
140
+
141
+ const response = await addProjectFromRegistry(client, projectData);
142
+
143
+ console.log('');
144
+ console.log(`✅ Project installed successfully!`);
145
+ console.log(` Project IDN: ${finalProjectIdn}`);
146
+ console.log(` Project ID: ${response.id}`);
147
+ console.log(` Source: ${registryItemIdn} v${selectedItem.version}`);
148
+ console.log(` Registry: ${registryIdn}`);
149
+ if (isAutoUpdateEnabled) {
150
+ console.log(` Auto-update: Enabled`);
151
+ }
152
+ console.log('');
153
+ console.log(`💡 Run "newo pull" to sync the project locally`);
154
+
155
+ } catch (error: unknown) {
156
+ console.error('❌ Failed to add project from registry:', error instanceof Error ? error.message : String(error));
157
+ process.exit(1);
158
+ }
159
+ }