newo 3.0.0 → 3.2.0

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 (57) hide show
  1. package/CHANGELOG.md +475 -347
  2. package/README.md +111 -0
  3. package/dist/api.d.ts +20 -1
  4. package/dist/api.js +110 -0
  5. package/dist/auth.js +4 -0
  6. package/dist/cli/commands/help.js +30 -1
  7. package/dist/cli/commands/list-actions.d.ts +3 -0
  8. package/dist/cli/commands/list-actions.js +89 -0
  9. package/dist/cli/commands/profile.d.ts +3 -0
  10. package/dist/cli/commands/profile.js +62 -0
  11. package/dist/cli/commands/pull-akb.d.ts +3 -0
  12. package/dist/cli/commands/pull-akb.js +19 -0
  13. package/dist/cli/commands/pull-attributes.js +7 -0
  14. package/dist/cli/commands/pull-integrations.d.ts +3 -0
  15. package/dist/cli/commands/pull-integrations.js +19 -0
  16. package/dist/cli/commands/push-akb.d.ts +3 -0
  17. package/dist/cli/commands/push-akb.js +19 -0
  18. package/dist/cli/commands/push-integrations.d.ts +3 -0
  19. package/dist/cli/commands/push-integrations.js +19 -0
  20. package/dist/cli/commands/sandbox.d.ts +14 -0
  21. package/dist/cli/commands/sandbox.js +306 -0
  22. package/dist/cli.js +33 -0
  23. package/dist/sandbox/chat.d.ts +40 -0
  24. package/dist/sandbox/chat.js +280 -0
  25. package/dist/sync/akb.d.ts +14 -0
  26. package/dist/sync/akb.js +175 -0
  27. package/dist/sync/attributes.d.ts +19 -0
  28. package/dist/sync/attributes.js +221 -2
  29. package/dist/sync/integrations.d.ts +23 -0
  30. package/dist/sync/integrations.js +340 -0
  31. package/dist/sync/projects.js +171 -1
  32. package/dist/sync/push.js +15 -0
  33. package/dist/sync/skill-files.js +1 -1
  34. package/dist/sync/status.js +4 -2
  35. package/dist/types.d.ts +209 -0
  36. package/package.json +14 -3
  37. package/src/api.ts +186 -1
  38. package/src/auth.ts +7 -2
  39. package/src/cli/commands/help.ts +30 -1
  40. package/src/cli/commands/list-actions.ts +112 -0
  41. package/src/cli/commands/profile.ts +79 -0
  42. package/src/cli/commands/pull-akb.ts +27 -0
  43. package/src/cli/commands/pull-attributes.ts +8 -0
  44. package/src/cli/commands/pull-integrations.ts +27 -0
  45. package/src/cli/commands/push-akb.ts +27 -0
  46. package/src/cli/commands/push-integrations.ts +27 -0
  47. package/src/cli/commands/sandbox.ts +365 -0
  48. package/src/cli.ts +41 -0
  49. package/src/sandbox/chat.ts +339 -0
  50. package/src/sync/akb.ts +205 -0
  51. package/src/sync/attributes.ts +269 -2
  52. package/src/sync/integrations.ts +403 -0
  53. package/src/sync/projects.ts +207 -1
  54. package/src/sync/push.ts +17 -0
  55. package/src/sync/skill-files.ts +1 -1
  56. package/src/sync/status.ts +4 -2
  57. package/src/types.ts +248 -0
@@ -1,13 +1,15 @@
1
1
  /**
2
- * Customer attributes synchronization module
2
+ * Customer and project attributes synchronization module
3
3
  */
4
- import { getCustomerAttributes } from '../api.js';
4
+ import { getCustomerAttributes, getProjectAttributes, listProjects, updateProjectAttribute } from '../api.js';
5
5
  import {
6
6
  writeFileSafe,
7
7
  customerAttributesPath,
8
8
  customerAttributesMapPath,
9
9
  customerAttributesBackupPath
10
10
  } from '../fsutil.js';
11
+ import path from 'path';
12
+ import fs from 'fs-extra';
11
13
  import yaml from 'js-yaml';
12
14
  import type { AxiosInstance } from 'axios';
13
15
  import type { CustomerConfig } from '../types.js';
@@ -107,4 +109,269 @@ export async function saveCustomerAttributes(
107
109
  console.error(`āŒ Failed to save customer attributes for ${customer.idn}:`, error);
108
110
  throw error;
109
111
  }
112
+ }
113
+
114
+ /**
115
+ * Save project attributes to YAML format in project directory
116
+ */
117
+ export async function saveProjectAttributes(
118
+ client: AxiosInstance,
119
+ customer: CustomerConfig,
120
+ projectId: string,
121
+ projectIdn: string,
122
+ verbose: boolean = false
123
+ ): Promise<void> {
124
+ if (verbose) console.log(` šŸ” Fetching project attributes for ${projectIdn}...`);
125
+
126
+ try {
127
+ const response = await getProjectAttributes(client, projectId, true); // Include hidden attributes
128
+
129
+ // API returns { groups: [...], attributes: [...] }
130
+ const attributes = response.attributes || response;
131
+ if (verbose) console.log(` šŸ“¦ Found ${Array.isArray(attributes) ? attributes.length : 0} project attributes`);
132
+
133
+ if (!Array.isArray(attributes) || attributes.length === 0) {
134
+ if (verbose) console.log(` ℹ No project attributes found for ${projectIdn}`);
135
+ return;
136
+ }
137
+
138
+ // Create ID mapping for push operations
139
+ const idMapping: Record<string, string> = {};
140
+
141
+ // Transform attributes to match format (no ID fields)
142
+ const cleanAttributes = attributes.map(attr => {
143
+ // Store ID mapping
144
+ if (attr.id) {
145
+ idMapping[attr.idn] = attr.id;
146
+ }
147
+
148
+ // Special handling for complex JSON string values
149
+ let processedValue = attr.value;
150
+ if (typeof attr.value === 'string' && attr.value.startsWith('[{') && attr.value.endsWith('}]')) {
151
+ try {
152
+ const parsed = JSON.parse(attr.value);
153
+ processedValue = JSON.stringify(parsed, null, 0);
154
+ } catch (e) {
155
+ processedValue = attr.value;
156
+ }
157
+ }
158
+
159
+ return {
160
+ idn: attr.idn,
161
+ value: processedValue,
162
+ title: attr.title || "",
163
+ description: attr.description || "",
164
+ group: attr.group || "",
165
+ is_hidden: attr.is_hidden,
166
+ possible_values: attr.possible_values || [],
167
+ value_type: `__ENUM_PLACEHOLDER_${attr.value_type}__`
168
+ };
169
+ });
170
+
171
+ const attributesYaml = {
172
+ attributes: cleanAttributes
173
+ };
174
+
175
+ // Configure YAML output
176
+ let yamlContent = yaml.dump(attributesYaml, {
177
+ indent: 2,
178
+ quotingType: '"',
179
+ forceQuotes: false,
180
+ lineWidth: 80,
181
+ noRefs: true,
182
+ sortKeys: false,
183
+ flowLevel: -1
184
+ });
185
+
186
+ // Post-process to fix enum format
187
+ yamlContent = yamlContent.replace(/__ENUM_PLACEHOLDER_(\w+)__/g, '!enum "AttributeValueTypes.$1"');
188
+ yamlContent = yamlContent.replace(/\\"/g, '"');
189
+
190
+ // Save to project directory
191
+ const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn);
192
+ const projectDir = path.join(customerDir, 'projects', projectIdn);
193
+ await fs.ensureDir(projectDir);
194
+
195
+ const attributesFile = path.join(projectDir, 'attributes.yaml');
196
+ const attributesMapFile = path.join(customerDir, '.newo', customer.idn, `project_${projectIdn}_attributes-map.json`);
197
+
198
+ await writeFileSafe(attributesFile, yamlContent);
199
+ await fs.ensureDir(path.dirname(attributesMapFile));
200
+ await writeFileSafe(attributesMapFile, JSON.stringify(idMapping, null, 2));
201
+
202
+ if (verbose) {
203
+ console.log(` āœ“ Saved project attributes to projects/${projectIdn}/attributes.yaml`);
204
+ console.log(` āœ“ Saved attribute ID mapping`);
205
+ }
206
+ } catch (error: any) {
207
+ if (verbose) console.error(` āŒ Failed to fetch project attributes for ${projectIdn}:`, error.message);
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Pull all project attributes for a customer
213
+ */
214
+ export async function pullAllProjectAttributes(
215
+ client: AxiosInstance,
216
+ customer: CustomerConfig,
217
+ verbose: boolean = false
218
+ ): Promise<void> {
219
+ if (verbose) console.log(`\nšŸ“‹ Fetching project attributes...`);
220
+
221
+ try {
222
+ // Get all projects for this customer
223
+ const projects = await listProjects(client);
224
+ if (verbose) console.log(`āœ“ Found ${projects.length} projects\n`);
225
+
226
+ for (const project of projects) {
227
+ await saveProjectAttributes(client, customer, project.id, project.idn, verbose);
228
+ }
229
+
230
+ if (verbose) console.log(`\nāœ… Completed project attributes sync for ${projects.length} projects\n`);
231
+ } catch (error) {
232
+ console.error(`āŒ Failed to pull project attributes:`, error);
233
+ throw error;
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Push modified project attributes for a specific project
239
+ */
240
+ export async function pushProjectAttributes(
241
+ client: AxiosInstance,
242
+ customer: CustomerConfig,
243
+ projectId: string,
244
+ projectIdn: string,
245
+ verbose: boolean = false
246
+ ): Promise<number> {
247
+ const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn);
248
+ const attributesFile = path.join(customerDir, 'projects', projectIdn, 'attributes.yaml');
249
+ const attributesMapFile = path.join(customerDir, '.newo', customer.idn, `project_${projectIdn}_attributes-map.json`);
250
+
251
+ // Check if attributes file exists
252
+ if (!await fs.pathExists(attributesFile)) {
253
+ if (verbose) console.log(` ℹ No project attributes file for ${projectIdn}`);
254
+ return 0;
255
+ }
256
+
257
+ // Load local attributes
258
+ // Read as text and replace the custom !enum tags before parsing
259
+ let attributesContent = await fs.readFile(attributesFile, 'utf-8');
260
+ // Replace custom enum tags with the actual value
261
+ attributesContent = attributesContent.replace(/!enum "AttributeValueTypes\.(\w+)"/g, '$1');
262
+
263
+ const localData = yaml.load(attributesContent) as { attributes: any[] };
264
+ const localAttributes = localData.attributes || [];
265
+
266
+ // Load ID mapping
267
+ if (!await fs.pathExists(attributesMapFile)) {
268
+ if (verbose) console.log(` ⚠ No ID mapping found for project ${projectIdn}, skipping push`);
269
+ return 0;
270
+ }
271
+
272
+ const idMapping = JSON.parse(await fs.readFile(attributesMapFile, 'utf-8')) as Record<string, string>;
273
+
274
+ // Fetch current remote attributes for comparison
275
+ const remoteResponse = await getProjectAttributes(client, projectId, true);
276
+ const remoteAttributes = remoteResponse.attributes || [];
277
+
278
+ // Create map of remote attributes by IDN
279
+ const remoteMap = new Map<string, any>();
280
+ remoteAttributes.forEach(attr => remoteMap.set(attr.idn, attr));
281
+
282
+ let updatedCount = 0;
283
+
284
+ // Check each local attribute for changes
285
+ for (const localAttr of localAttributes) {
286
+ const attributeId = idMapping[localAttr.idn];
287
+ if (!attributeId) {
288
+ if (verbose) console.log(` ⚠ No ID mapping for attribute ${localAttr.idn}, skipping`);
289
+ continue;
290
+ }
291
+
292
+ const remoteAttr = remoteMap.get(localAttr.idn);
293
+ if (!remoteAttr) {
294
+ if (verbose) console.log(` ⚠ Attribute ${localAttr.idn} not found remotely, skipping`);
295
+ continue;
296
+ }
297
+
298
+ // Value type is already parsed (we removed !enum tags above)
299
+ const valueType = localAttr.value_type;
300
+
301
+ // Check if value changed
302
+ const localValue = String(localAttr.value || '');
303
+ const remoteValue = String(remoteAttr.value || '');
304
+
305
+ if (localValue !== remoteValue) {
306
+ if (verbose) console.log(` šŸ”„ Updating project attribute: ${localAttr.idn}`);
307
+
308
+ try {
309
+ const attributeToUpdate = {
310
+ id: attributeId,
311
+ idn: localAttr.idn,
312
+ value: localAttr.value,
313
+ title: localAttr.title,
314
+ description: localAttr.description,
315
+ group: localAttr.group,
316
+ is_hidden: localAttr.is_hidden,
317
+ possible_values: localAttr.possible_values,
318
+ value_type: valueType
319
+ };
320
+
321
+ await updateProjectAttribute(client, projectId, attributeToUpdate);
322
+ if (verbose) console.log(` āœ… Updated: ${localAttr.idn} (${localAttr.title})`);
323
+ updatedCount++;
324
+ } catch (error: any) {
325
+ const errorDetail = error.response?.data || error.message;
326
+ console.error(` āŒ Failed to update ${localAttr.idn}: ${JSON.stringify(errorDetail)}`);
327
+ if (verbose) {
328
+ console.error(` API response:`, error.response?.status, error.response?.statusText);
329
+ console.error(` Endpoint tried: PUT /api/v1/project/attributes/${attributeId}`);
330
+ }
331
+ }
332
+ } else if (verbose) {
333
+ console.log(` āœ“ No changes: ${localAttr.idn}`);
334
+ }
335
+ }
336
+
337
+ return updatedCount;
338
+ }
339
+
340
+ /**
341
+ * Push all modified project attributes for all projects
342
+ */
343
+ export async function pushAllProjectAttributes(
344
+ client: AxiosInstance,
345
+ customer: CustomerConfig,
346
+ projectsMap: Record<string, { projectId: string; projectIdn: string }>,
347
+ verbose: boolean = false
348
+ ): Promise<number> {
349
+ if (verbose) console.log(`\nšŸ“‹ Checking project attributes for changes...`);
350
+
351
+ let totalUpdated = 0;
352
+
353
+ for (const [projectIdn, projectInfo] of Object.entries(projectsMap)) {
354
+ if (!projectIdn) continue; // Skip empty project idn (legacy format)
355
+
356
+ if (verbose) console.log(`\n šŸ“ Project: ${projectIdn}`);
357
+
358
+ const updated = await pushProjectAttributes(
359
+ client,
360
+ customer,
361
+ projectInfo.projectId,
362
+ projectInfo.projectIdn || projectIdn,
363
+ verbose
364
+ );
365
+
366
+ totalUpdated += updated;
367
+ }
368
+
369
+ // Always show summary if changes were made
370
+ if (totalUpdated > 0) {
371
+ console.log(`\nāœ… Updated ${totalUpdated} project attribute(s)`);
372
+ } else {
373
+ if (verbose) console.log(`\nāœ“ No project attribute changes to push`);
374
+ }
375
+
376
+ return totalUpdated;
110
377
  }
@@ -0,0 +1,403 @@
1
+ /**
2
+ * Integration and connector synchronization module
3
+ * Handles pull/push of integrations and connectors to/from NEWO platform
4
+ */
5
+
6
+ import path from 'path';
7
+ import fs from 'fs-extra';
8
+ import yaml from 'js-yaml';
9
+ import type { AxiosInstance } from 'axios';
10
+ import {
11
+ listIntegrations,
12
+ listConnectors,
13
+ getIntegrationSettings,
14
+ createConnector,
15
+ updateConnector,
16
+ deleteConnector,
17
+ listOutgoingWebhooks,
18
+ listIncomingWebhooks
19
+ } from '../api.js';
20
+ import type {
21
+ Connector,
22
+ IntegrationMetadata,
23
+ ConnectorMetadata,
24
+ IntegrationsYamlData,
25
+ OutgoingWebhook,
26
+ IncomingWebhook
27
+ } from '../types.js';
28
+
29
+ /**
30
+ * Pull all integrations and connectors from NEWO platform
31
+ */
32
+ export async function pullIntegrations(
33
+ client: AxiosInstance,
34
+ customerDir: string,
35
+ verbose: boolean = false
36
+ ): Promise<void> {
37
+ if (verbose) console.log('\nšŸ“¦ Pulling integrations from NEWO platform...\n');
38
+
39
+ // Create integrations directory
40
+ const integrationsDir = path.join(customerDir, 'integrations');
41
+ await fs.ensureDir(integrationsDir);
42
+
43
+ // Fetch all integrations
44
+ const integrations = await listIntegrations(client);
45
+ if (verbose) console.log(`āœ“ Found ${integrations.length} integrations`);
46
+
47
+ const integrationsMetadata: IntegrationMetadata[] = [];
48
+
49
+ // Process each integration
50
+ for (const integration of integrations) {
51
+ if (verbose) console.log(`\n šŸ“¦ Processing: ${integration.title} (${integration.idn})`);
52
+
53
+ // Add to metadata list
54
+ integrationsMetadata.push({
55
+ id: integration.id,
56
+ idn: integration.idn,
57
+ title: integration.title,
58
+ description: integration.description,
59
+ channel: integration.channel,
60
+ is_disabled: integration.is_disabled
61
+ });
62
+
63
+ // Create integration directory
64
+ const integrationDir = path.join(integrationsDir, integration.idn);
65
+ await fs.ensureDir(integrationDir);
66
+
67
+ // Fetch integration settings
68
+ let integrationSettings: any[] = [];
69
+ try {
70
+ integrationSettings = await getIntegrationSettings(client, integration.id);
71
+ } catch (error: any) {
72
+ // Settings endpoint may not be available for all integrations
73
+ if (verbose && error.response?.status !== 404) {
74
+ console.log(` ⚠ Could not fetch settings: ${error.message}`);
75
+ }
76
+ }
77
+
78
+ // Save combined integration file (metadata + settings)
79
+ const integrationFile = path.join(integrationDir, `${integration.idn}.yaml`);
80
+ const integrationData: any = {
81
+ id: integration.id,
82
+ idn: integration.idn,
83
+ title: integration.title,
84
+ description: integration.description,
85
+ channel: integration.channel,
86
+ is_disabled: integration.is_disabled
87
+ };
88
+
89
+ // Add settings array if any settings exist
90
+ if (integrationSettings.length > 0) {
91
+ integrationData.settings = integrationSettings;
92
+ }
93
+
94
+ await fs.writeFile(integrationFile, yaml.dump(integrationData, { lineWidth: -1 }));
95
+ if (verbose) console.log(` āœ“ Saved integration → ${integration.idn}.yaml (${integrationSettings.length} settings)`);
96
+
97
+ // Fetch and save connectors
98
+ const connectors = await listConnectors(client, integration.id);
99
+ if (verbose) console.log(` Connectors: ${connectors.length} found`);
100
+
101
+ if (connectors.length > 0) {
102
+ const connectorsDir = path.join(integrationDir, 'connectors');
103
+ await fs.ensureDir(connectorsDir);
104
+
105
+ for (const connector of connectors) {
106
+ const connectorMetadata: ConnectorMetadata = {
107
+ id: connector.id,
108
+ connector_idn: connector.connector_idn,
109
+ title: connector.title,
110
+ status: connector.status,
111
+ integration_idn: integration.idn,
112
+ settings: connector.settings
113
+ };
114
+
115
+ // Create subdirectory for this connector
116
+ const connectorDir = path.join(connectorsDir, connector.connector_idn);
117
+ await fs.ensureDir(connectorDir);
118
+
119
+ // Save connector YAML file inside its subdirectory
120
+ const connectorFile = path.join(connectorDir, `${connector.connector_idn}.yaml`);
121
+ await fs.writeFile(connectorFile, yaml.dump(connectorMetadata, { lineWidth: -1 }));
122
+
123
+ if (verbose) console.log(` āœ“ Saved: ${connector.title} → connectors/${connector.connector_idn}/${connector.connector_idn}.yaml`);
124
+ }
125
+ }
126
+ }
127
+
128
+ // Fetch and save webhooks (for API integration connectors only)
129
+ if (verbose) console.log(`\nšŸ“” Fetching webhooks...`);
130
+
131
+ try {
132
+ const outgoingWebhooks = await listOutgoingWebhooks(client);
133
+ const incomingWebhooks = await listIncomingWebhooks(client);
134
+
135
+ if (verbose) console.log(`āœ“ Found ${outgoingWebhooks.length} outgoing webhooks`);
136
+ if (verbose) console.log(`āœ“ Found ${incomingWebhooks.length} incoming webhooks`);
137
+
138
+ // Group webhooks by connector_idn
139
+ const outgoingByConnector = new Map<string, OutgoingWebhook[]>();
140
+ const incomingByConnector = new Map<string, IncomingWebhook[]>();
141
+
142
+ outgoingWebhooks.forEach(webhook => {
143
+ if (!outgoingByConnector.has(webhook.connector_idn)) {
144
+ outgoingByConnector.set(webhook.connector_idn, []);
145
+ }
146
+ outgoingByConnector.get(webhook.connector_idn)!.push(webhook);
147
+ });
148
+
149
+ incomingWebhooks.forEach(webhook => {
150
+ if (!incomingByConnector.has(webhook.connector_idn)) {
151
+ incomingByConnector.set(webhook.connector_idn, []);
152
+ }
153
+ incomingByConnector.get(webhook.connector_idn)!.push(webhook);
154
+ });
155
+
156
+ // Save webhooks to appropriate connector directories
157
+ for (const integration of integrations) {
158
+ const integrationDir = path.join(integrationsDir, integration.idn);
159
+ const connectorsDir = path.join(integrationDir, 'connectors');
160
+
161
+ if (await fs.pathExists(connectorsDir)) {
162
+ const connectors = await listConnectors(client, integration.id);
163
+
164
+ for (const connector of connectors) {
165
+ const connectorWebhooksDir = path.join(connectorsDir, connector.connector_idn, 'webhooks');
166
+
167
+ const outgoing = outgoingByConnector.get(connector.connector_idn) || [];
168
+ const incoming = incomingByConnector.get(connector.connector_idn) || [];
169
+
170
+ if (outgoing.length > 0 || incoming.length > 0) {
171
+ await fs.ensureDir(connectorWebhooksDir);
172
+
173
+ if (outgoing.length > 0) {
174
+ const outgoingFile = path.join(connectorWebhooksDir, 'outgoing.yaml');
175
+ await fs.writeFile(outgoingFile, yaml.dump({ webhooks: outgoing }, { lineWidth: -1 }));
176
+ if (verbose) console.log(` āœ“ Saved: ${outgoing.length} outgoing webhooks → ${connector.connector_idn}/webhooks/outgoing.yaml`);
177
+ }
178
+
179
+ if (incoming.length > 0) {
180
+ const incomingFile = path.join(connectorWebhooksDir, 'incoming.yaml');
181
+ await fs.writeFile(incomingFile, yaml.dump({ webhooks: incoming }, { lineWidth: -1 }));
182
+ if (verbose) console.log(` āœ“ Saved: ${incoming.length} incoming webhooks → ${connector.connector_idn}/webhooks/incoming.yaml`);
183
+ }
184
+ }
185
+ }
186
+ }
187
+ }
188
+ } catch (error: any) {
189
+ if (verbose) console.log(`⚠ Could not fetch webhooks: ${error.message}`);
190
+ }
191
+
192
+ // Save master integrations list
193
+ const integrationsData: IntegrationsYamlData = { integrations: integrationsMetadata };
194
+ const integrationsFile = path.join(integrationsDir, 'integrations.yaml');
195
+ await fs.writeFile(integrationsFile, yaml.dump(integrationsData, { lineWidth: -1 }));
196
+
197
+ if (verbose) console.log(`\nāœ… Saved ${integrations.length} integrations to integrations/integrations.yaml\n`);
198
+ }
199
+
200
+ /**
201
+ * Push integration changes from local files to NEWO platform
202
+ */
203
+ export async function pushIntegrations(
204
+ client: AxiosInstance,
205
+ customerDir: string,
206
+ verbose: boolean = false
207
+ ): Promise<void> {
208
+ if (verbose) console.log('\nšŸ“¤ Pushing integration changes to NEWO platform...\n');
209
+
210
+ const integrationsDir = path.join(customerDir, 'integrations');
211
+
212
+ // Check if integrations directory exists
213
+ if (!await fs.pathExists(integrationsDir)) {
214
+ if (verbose) console.log('⚠ No integrations directory found. Run pull-integrations first.');
215
+ return;
216
+ }
217
+
218
+ // Load remote integrations for ID mapping
219
+ const remoteIntegrations = await listIntegrations(client);
220
+ const integrationMap = new Map<string, string>(); // idn -> id
221
+ remoteIntegrations.forEach(int => integrationMap.set(int.idn, int.id));
222
+
223
+ let updatedCount = 0;
224
+ let createdCount = 0;
225
+ let deletedCount = 0;
226
+
227
+ // Read integrations directory
228
+ const integrationFolders = await fs.readdir(integrationsDir);
229
+
230
+ for (const folder of integrationFolders) {
231
+ if (folder === 'integrations.yaml') continue; // Skip master file
232
+
233
+ const integrationDir = path.join(integrationsDir, folder);
234
+ const stat = await fs.stat(integrationDir);
235
+ if (!stat.isDirectory()) continue;
236
+
237
+ const integrationIdn = folder;
238
+ const integrationId = integrationMap.get(integrationIdn);
239
+
240
+ if (!integrationId) {
241
+ if (verbose) console.log(`⚠ Integration ${integrationIdn} not found on platform, skipping...`);
242
+ continue;
243
+ }
244
+
245
+ if (verbose) console.log(`\n šŸ“¦ Processing: ${integrationIdn}`);
246
+
247
+ // Process connectors
248
+ const connectorsDir = path.join(integrationDir, 'connectors');
249
+ if (await fs.pathExists(connectorsDir)) {
250
+ // Load remote connectors for comparison
251
+ const remoteConnectors = await listConnectors(client, integrationId);
252
+ const remoteConnectorMap = new Map<string, Connector>();
253
+ remoteConnectors.forEach(conn => remoteConnectorMap.set(conn.connector_idn, conn));
254
+
255
+ // Read connector subdirectories
256
+ const connectorDirs = await fs.readdir(connectorsDir);
257
+ const localConnectorIdns = new Set<string>();
258
+
259
+ for (const connectorDirName of connectorDirs) {
260
+ const connectorPath = path.join(connectorsDir, connectorDirName);
261
+ const stat = await fs.stat(connectorPath);
262
+ if (!stat.isDirectory()) continue; // Skip non-directories
263
+
264
+ // Read connector YAML file from within the subdirectory
265
+ const connectorFile = path.join(connectorPath, `${connectorDirName}.yaml`);
266
+ if (!await fs.pathExists(connectorFile)) {
267
+ if (verbose) console.log(` ⚠ No YAML file found in ${connectorDirName}/, skipping...`);
268
+ continue;
269
+ }
270
+
271
+ const connectorData = yaml.load(await fs.readFile(connectorFile, 'utf-8')) as ConnectorMetadata;
272
+
273
+ localConnectorIdns.add(connectorData.connector_idn);
274
+
275
+ const remoteConnector = remoteConnectorMap.get(connectorData.connector_idn);
276
+
277
+ if (!remoteConnector) {
278
+ // Create new connector
279
+ if (verbose) console.log(` āž• Creating connector: ${connectorData.title}`);
280
+ try {
281
+ await createConnector(client, integrationId, {
282
+ title: connectorData.title,
283
+ connector_idn: connectorData.connector_idn,
284
+ integration_idn: integrationIdn,
285
+ settings: connectorData.settings
286
+ });
287
+ createdCount++;
288
+ if (verbose) console.log(` āœ… Created: ${connectorData.title}`);
289
+ } catch (error: any) {
290
+ console.error(` āŒ Failed to create connector: ${error.message}`);
291
+ }
292
+ } else {
293
+ // Check if connector needs update
294
+ const needsUpdate = hasConnectorChanged(remoteConnector, connectorData);
295
+
296
+ if (needsUpdate) {
297
+ if (verbose) console.log(` šŸ”„ Updating connector: ${connectorData.title}`);
298
+ try {
299
+ await updateConnector(client, remoteConnector.id, {
300
+ title: connectorData.title,
301
+ status: connectorData.status,
302
+ settings: connectorData.settings
303
+ });
304
+ updatedCount++;
305
+ if (verbose) console.log(` āœ… Updated: ${connectorData.title}`);
306
+ } catch (error: any) {
307
+ console.error(` āŒ Failed to update connector: ${error.message}`);
308
+ }
309
+ } else {
310
+ if (verbose) console.log(` āœ“ No changes: ${connectorData.title}`);
311
+ }
312
+ }
313
+ }
314
+
315
+ // Delete connectors that exist remotely but not locally
316
+ for (const [connectorIdn, remoteConnector] of remoteConnectorMap) {
317
+ if (!localConnectorIdns.has(connectorIdn)) {
318
+ if (verbose) console.log(` šŸ—‘ļø Deleting connector: ${remoteConnector.title}`);
319
+ try {
320
+ await deleteConnector(client, remoteConnector.id);
321
+ deletedCount++;
322
+ if (verbose) console.log(` āœ… Deleted: ${remoteConnector.title}`);
323
+ } catch (error: any) {
324
+ console.error(` āŒ Failed to delete connector: ${error.message}`);
325
+ }
326
+ }
327
+ }
328
+ }
329
+ }
330
+
331
+ // Always show summary if changes were made, not just in verbose mode
332
+ if (createdCount > 0 || updatedCount > 0 || deletedCount > 0) {
333
+ console.log(`\nāœ… Integration push completed:`);
334
+ console.log(` Created: ${createdCount} connector(s)`);
335
+ console.log(` Updated: ${updatedCount} connector(s)`);
336
+ console.log(` Deleted: ${deletedCount} connector(s)`);
337
+ } else {
338
+ console.log(`\nāœ“ No connector changes to push`);
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Check if connector has changed compared to remote version
344
+ */
345
+ function hasConnectorChanged(remote: Connector, local: ConnectorMetadata): boolean {
346
+ // Check title
347
+ if (remote.title !== local.title) return true;
348
+
349
+ // Check status
350
+ if (remote.status !== local.status) return true;
351
+
352
+ // Check settings
353
+ if (remote.settings.length !== local.settings.length) return true;
354
+
355
+ // Compare each setting
356
+ const remoteSettingsMap = new Map<string, string>();
357
+ remote.settings.forEach(s => remoteSettingsMap.set(s.idn, s.value));
358
+
359
+ for (const localSetting of local.settings) {
360
+ const remoteValue = remoteSettingsMap.get(localSetting.idn);
361
+ if (remoteValue !== localSetting.value) return true;
362
+ }
363
+
364
+ return false;
365
+ }
366
+
367
+ /**
368
+ * List all local integrations from file system
369
+ */
370
+ export async function listLocalIntegrations(customerDir: string): Promise<IntegrationMetadata[]> {
371
+ const integrationsFile = path.join(customerDir, 'integrations', 'integrations.yaml');
372
+
373
+ if (!await fs.pathExists(integrationsFile)) {
374
+ return [];
375
+ }
376
+
377
+ const data = yaml.load(await fs.readFile(integrationsFile, 'utf-8')) as IntegrationsYamlData;
378
+ return data.integrations;
379
+ }
380
+
381
+ /**
382
+ * Get connector details from local file
383
+ */
384
+ export async function getLocalConnector(
385
+ customerDir: string,
386
+ integrationIdn: string,
387
+ connectorIdn: string
388
+ ): Promise<ConnectorMetadata | null> {
389
+ const connectorFile = path.join(
390
+ customerDir,
391
+ 'integrations',
392
+ integrationIdn,
393
+ 'connectors',
394
+ connectorIdn,
395
+ `${connectorIdn}.yaml`
396
+ );
397
+
398
+ if (!await fs.pathExists(connectorFile)) {
399
+ return null;
400
+ }
401
+
402
+ return yaml.load(await fs.readFile(connectorFile, 'utf-8')) as ConnectorMetadata;
403
+ }