newo 3.1.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.
- package/CHANGELOG.md +139 -1
- package/dist/api.d.ts +14 -1
- package/dist/api.js +74 -0
- package/dist/cli/commands/help.js +20 -1
- package/dist/cli/commands/list-actions.d.ts +3 -0
- package/dist/cli/commands/list-actions.js +89 -0
- package/dist/cli/commands/profile.d.ts +3 -0
- package/dist/cli/commands/profile.js +62 -0
- package/dist/cli/commands/pull-akb.d.ts +3 -0
- package/dist/cli/commands/pull-akb.js +19 -0
- package/dist/cli/commands/pull-attributes.js +7 -0
- package/dist/cli/commands/pull-integrations.d.ts +3 -0
- package/dist/cli/commands/pull-integrations.js +19 -0
- package/dist/cli/commands/push-akb.d.ts +3 -0
- package/dist/cli/commands/push-akb.js +19 -0
- package/dist/cli/commands/push-integrations.d.ts +3 -0
- package/dist/cli/commands/push-integrations.js +19 -0
- package/dist/cli.js +24 -0
- package/dist/sync/akb.d.ts +14 -0
- package/dist/sync/akb.js +175 -0
- package/dist/sync/attributes.d.ts +19 -0
- package/dist/sync/attributes.js +221 -2
- package/dist/sync/integrations.d.ts +23 -0
- package/dist/sync/integrations.js +340 -0
- package/dist/sync/projects.js +171 -1
- package/dist/sync/push.js +15 -0
- package/dist/sync/skill-files.js +1 -1
- package/dist/sync/status.js +4 -2
- package/dist/types.d.ts +128 -0
- package/package.json +11 -3
- package/src/api.ts +132 -1
- package/src/cli/commands/help.ts +20 -1
- package/src/cli/commands/list-actions.ts +112 -0
- package/src/cli/commands/profile.ts +79 -0
- package/src/cli/commands/pull-akb.ts +27 -0
- package/src/cli/commands/pull-attributes.ts +8 -0
- package/src/cli/commands/pull-integrations.ts +27 -0
- package/src/cli/commands/push-akb.ts +27 -0
- package/src/cli/commands/push-integrations.ts +27 -0
- package/src/cli.ts +30 -0
- package/src/sync/akb.ts +205 -0
- package/src/sync/attributes.ts +269 -2
- package/src/sync/integrations.ts +403 -0
- package/src/sync/projects.ts +207 -1
- package/src/sync/push.ts +17 -0
- package/src/sync/skill-files.ts +1 -1
- package/src/sync/status.ts +4 -2
- package/src/types.ts +150 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration and connector synchronization module
|
|
3
|
+
* Handles pull/push of integrations and connectors to/from NEWO platform
|
|
4
|
+
*/
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import yaml from 'js-yaml';
|
|
8
|
+
import { listIntegrations, listConnectors, getIntegrationSettings, createConnector, updateConnector, deleteConnector, listOutgoingWebhooks, listIncomingWebhooks } from '../api.js';
|
|
9
|
+
/**
|
|
10
|
+
* Pull all integrations and connectors from NEWO platform
|
|
11
|
+
*/
|
|
12
|
+
export async function pullIntegrations(client, customerDir, verbose = false) {
|
|
13
|
+
if (verbose)
|
|
14
|
+
console.log('\nš¦ Pulling integrations from NEWO platform...\n');
|
|
15
|
+
// Create integrations directory
|
|
16
|
+
const integrationsDir = path.join(customerDir, 'integrations');
|
|
17
|
+
await fs.ensureDir(integrationsDir);
|
|
18
|
+
// Fetch all integrations
|
|
19
|
+
const integrations = await listIntegrations(client);
|
|
20
|
+
if (verbose)
|
|
21
|
+
console.log(`ā Found ${integrations.length} integrations`);
|
|
22
|
+
const integrationsMetadata = [];
|
|
23
|
+
// Process each integration
|
|
24
|
+
for (const integration of integrations) {
|
|
25
|
+
if (verbose)
|
|
26
|
+
console.log(`\n š¦ Processing: ${integration.title} (${integration.idn})`);
|
|
27
|
+
// Add to metadata list
|
|
28
|
+
integrationsMetadata.push({
|
|
29
|
+
id: integration.id,
|
|
30
|
+
idn: integration.idn,
|
|
31
|
+
title: integration.title,
|
|
32
|
+
description: integration.description,
|
|
33
|
+
channel: integration.channel,
|
|
34
|
+
is_disabled: integration.is_disabled
|
|
35
|
+
});
|
|
36
|
+
// Create integration directory
|
|
37
|
+
const integrationDir = path.join(integrationsDir, integration.idn);
|
|
38
|
+
await fs.ensureDir(integrationDir);
|
|
39
|
+
// Fetch integration settings
|
|
40
|
+
let integrationSettings = [];
|
|
41
|
+
try {
|
|
42
|
+
integrationSettings = await getIntegrationSettings(client, integration.id);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
// Settings endpoint may not be available for all integrations
|
|
46
|
+
if (verbose && error.response?.status !== 404) {
|
|
47
|
+
console.log(` ā Could not fetch settings: ${error.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Save combined integration file (metadata + settings)
|
|
51
|
+
const integrationFile = path.join(integrationDir, `${integration.idn}.yaml`);
|
|
52
|
+
const integrationData = {
|
|
53
|
+
id: integration.id,
|
|
54
|
+
idn: integration.idn,
|
|
55
|
+
title: integration.title,
|
|
56
|
+
description: integration.description,
|
|
57
|
+
channel: integration.channel,
|
|
58
|
+
is_disabled: integration.is_disabled
|
|
59
|
+
};
|
|
60
|
+
// Add settings array if any settings exist
|
|
61
|
+
if (integrationSettings.length > 0) {
|
|
62
|
+
integrationData.settings = integrationSettings;
|
|
63
|
+
}
|
|
64
|
+
await fs.writeFile(integrationFile, yaml.dump(integrationData, { lineWidth: -1 }));
|
|
65
|
+
if (verbose)
|
|
66
|
+
console.log(` ā Saved integration ā ${integration.idn}.yaml (${integrationSettings.length} settings)`);
|
|
67
|
+
// Fetch and save connectors
|
|
68
|
+
const connectors = await listConnectors(client, integration.id);
|
|
69
|
+
if (verbose)
|
|
70
|
+
console.log(` Connectors: ${connectors.length} found`);
|
|
71
|
+
if (connectors.length > 0) {
|
|
72
|
+
const connectorsDir = path.join(integrationDir, 'connectors');
|
|
73
|
+
await fs.ensureDir(connectorsDir);
|
|
74
|
+
for (const connector of connectors) {
|
|
75
|
+
const connectorMetadata = {
|
|
76
|
+
id: connector.id,
|
|
77
|
+
connector_idn: connector.connector_idn,
|
|
78
|
+
title: connector.title,
|
|
79
|
+
status: connector.status,
|
|
80
|
+
integration_idn: integration.idn,
|
|
81
|
+
settings: connector.settings
|
|
82
|
+
};
|
|
83
|
+
// Create subdirectory for this connector
|
|
84
|
+
const connectorDir = path.join(connectorsDir, connector.connector_idn);
|
|
85
|
+
await fs.ensureDir(connectorDir);
|
|
86
|
+
// Save connector YAML file inside its subdirectory
|
|
87
|
+
const connectorFile = path.join(connectorDir, `${connector.connector_idn}.yaml`);
|
|
88
|
+
await fs.writeFile(connectorFile, yaml.dump(connectorMetadata, { lineWidth: -1 }));
|
|
89
|
+
if (verbose)
|
|
90
|
+
console.log(` ā Saved: ${connector.title} ā connectors/${connector.connector_idn}/${connector.connector_idn}.yaml`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Fetch and save webhooks (for API integration connectors only)
|
|
95
|
+
if (verbose)
|
|
96
|
+
console.log(`\nš” Fetching webhooks...`);
|
|
97
|
+
try {
|
|
98
|
+
const outgoingWebhooks = await listOutgoingWebhooks(client);
|
|
99
|
+
const incomingWebhooks = await listIncomingWebhooks(client);
|
|
100
|
+
if (verbose)
|
|
101
|
+
console.log(`ā Found ${outgoingWebhooks.length} outgoing webhooks`);
|
|
102
|
+
if (verbose)
|
|
103
|
+
console.log(`ā Found ${incomingWebhooks.length} incoming webhooks`);
|
|
104
|
+
// Group webhooks by connector_idn
|
|
105
|
+
const outgoingByConnector = new Map();
|
|
106
|
+
const incomingByConnector = new Map();
|
|
107
|
+
outgoingWebhooks.forEach(webhook => {
|
|
108
|
+
if (!outgoingByConnector.has(webhook.connector_idn)) {
|
|
109
|
+
outgoingByConnector.set(webhook.connector_idn, []);
|
|
110
|
+
}
|
|
111
|
+
outgoingByConnector.get(webhook.connector_idn).push(webhook);
|
|
112
|
+
});
|
|
113
|
+
incomingWebhooks.forEach(webhook => {
|
|
114
|
+
if (!incomingByConnector.has(webhook.connector_idn)) {
|
|
115
|
+
incomingByConnector.set(webhook.connector_idn, []);
|
|
116
|
+
}
|
|
117
|
+
incomingByConnector.get(webhook.connector_idn).push(webhook);
|
|
118
|
+
});
|
|
119
|
+
// Save webhooks to appropriate connector directories
|
|
120
|
+
for (const integration of integrations) {
|
|
121
|
+
const integrationDir = path.join(integrationsDir, integration.idn);
|
|
122
|
+
const connectorsDir = path.join(integrationDir, 'connectors');
|
|
123
|
+
if (await fs.pathExists(connectorsDir)) {
|
|
124
|
+
const connectors = await listConnectors(client, integration.id);
|
|
125
|
+
for (const connector of connectors) {
|
|
126
|
+
const connectorWebhooksDir = path.join(connectorsDir, connector.connector_idn, 'webhooks');
|
|
127
|
+
const outgoing = outgoingByConnector.get(connector.connector_idn) || [];
|
|
128
|
+
const incoming = incomingByConnector.get(connector.connector_idn) || [];
|
|
129
|
+
if (outgoing.length > 0 || incoming.length > 0) {
|
|
130
|
+
await fs.ensureDir(connectorWebhooksDir);
|
|
131
|
+
if (outgoing.length > 0) {
|
|
132
|
+
const outgoingFile = path.join(connectorWebhooksDir, 'outgoing.yaml');
|
|
133
|
+
await fs.writeFile(outgoingFile, yaml.dump({ webhooks: outgoing }, { lineWidth: -1 }));
|
|
134
|
+
if (verbose)
|
|
135
|
+
console.log(` ā Saved: ${outgoing.length} outgoing webhooks ā ${connector.connector_idn}/webhooks/outgoing.yaml`);
|
|
136
|
+
}
|
|
137
|
+
if (incoming.length > 0) {
|
|
138
|
+
const incomingFile = path.join(connectorWebhooksDir, 'incoming.yaml');
|
|
139
|
+
await fs.writeFile(incomingFile, yaml.dump({ webhooks: incoming }, { lineWidth: -1 }));
|
|
140
|
+
if (verbose)
|
|
141
|
+
console.log(` ā Saved: ${incoming.length} incoming webhooks ā ${connector.connector_idn}/webhooks/incoming.yaml`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (verbose)
|
|
150
|
+
console.log(`ā Could not fetch webhooks: ${error.message}`);
|
|
151
|
+
}
|
|
152
|
+
// Save master integrations list
|
|
153
|
+
const integrationsData = { integrations: integrationsMetadata };
|
|
154
|
+
const integrationsFile = path.join(integrationsDir, 'integrations.yaml');
|
|
155
|
+
await fs.writeFile(integrationsFile, yaml.dump(integrationsData, { lineWidth: -1 }));
|
|
156
|
+
if (verbose)
|
|
157
|
+
console.log(`\nā
Saved ${integrations.length} integrations to integrations/integrations.yaml\n`);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Push integration changes from local files to NEWO platform
|
|
161
|
+
*/
|
|
162
|
+
export async function pushIntegrations(client, customerDir, verbose = false) {
|
|
163
|
+
if (verbose)
|
|
164
|
+
console.log('\nš¤ Pushing integration changes to NEWO platform...\n');
|
|
165
|
+
const integrationsDir = path.join(customerDir, 'integrations');
|
|
166
|
+
// Check if integrations directory exists
|
|
167
|
+
if (!await fs.pathExists(integrationsDir)) {
|
|
168
|
+
if (verbose)
|
|
169
|
+
console.log('ā No integrations directory found. Run pull-integrations first.');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// Load remote integrations for ID mapping
|
|
173
|
+
const remoteIntegrations = await listIntegrations(client);
|
|
174
|
+
const integrationMap = new Map(); // idn -> id
|
|
175
|
+
remoteIntegrations.forEach(int => integrationMap.set(int.idn, int.id));
|
|
176
|
+
let updatedCount = 0;
|
|
177
|
+
let createdCount = 0;
|
|
178
|
+
let deletedCount = 0;
|
|
179
|
+
// Read integrations directory
|
|
180
|
+
const integrationFolders = await fs.readdir(integrationsDir);
|
|
181
|
+
for (const folder of integrationFolders) {
|
|
182
|
+
if (folder === 'integrations.yaml')
|
|
183
|
+
continue; // Skip master file
|
|
184
|
+
const integrationDir = path.join(integrationsDir, folder);
|
|
185
|
+
const stat = await fs.stat(integrationDir);
|
|
186
|
+
if (!stat.isDirectory())
|
|
187
|
+
continue;
|
|
188
|
+
const integrationIdn = folder;
|
|
189
|
+
const integrationId = integrationMap.get(integrationIdn);
|
|
190
|
+
if (!integrationId) {
|
|
191
|
+
if (verbose)
|
|
192
|
+
console.log(`ā Integration ${integrationIdn} not found on platform, skipping...`);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (verbose)
|
|
196
|
+
console.log(`\n š¦ Processing: ${integrationIdn}`);
|
|
197
|
+
// Process connectors
|
|
198
|
+
const connectorsDir = path.join(integrationDir, 'connectors');
|
|
199
|
+
if (await fs.pathExists(connectorsDir)) {
|
|
200
|
+
// Load remote connectors for comparison
|
|
201
|
+
const remoteConnectors = await listConnectors(client, integrationId);
|
|
202
|
+
const remoteConnectorMap = new Map();
|
|
203
|
+
remoteConnectors.forEach(conn => remoteConnectorMap.set(conn.connector_idn, conn));
|
|
204
|
+
// Read connector subdirectories
|
|
205
|
+
const connectorDirs = await fs.readdir(connectorsDir);
|
|
206
|
+
const localConnectorIdns = new Set();
|
|
207
|
+
for (const connectorDirName of connectorDirs) {
|
|
208
|
+
const connectorPath = path.join(connectorsDir, connectorDirName);
|
|
209
|
+
const stat = await fs.stat(connectorPath);
|
|
210
|
+
if (!stat.isDirectory())
|
|
211
|
+
continue; // Skip non-directories
|
|
212
|
+
// Read connector YAML file from within the subdirectory
|
|
213
|
+
const connectorFile = path.join(connectorPath, `${connectorDirName}.yaml`);
|
|
214
|
+
if (!await fs.pathExists(connectorFile)) {
|
|
215
|
+
if (verbose)
|
|
216
|
+
console.log(` ā No YAML file found in ${connectorDirName}/, skipping...`);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const connectorData = yaml.load(await fs.readFile(connectorFile, 'utf-8'));
|
|
220
|
+
localConnectorIdns.add(connectorData.connector_idn);
|
|
221
|
+
const remoteConnector = remoteConnectorMap.get(connectorData.connector_idn);
|
|
222
|
+
if (!remoteConnector) {
|
|
223
|
+
// Create new connector
|
|
224
|
+
if (verbose)
|
|
225
|
+
console.log(` ā Creating connector: ${connectorData.title}`);
|
|
226
|
+
try {
|
|
227
|
+
await createConnector(client, integrationId, {
|
|
228
|
+
title: connectorData.title,
|
|
229
|
+
connector_idn: connectorData.connector_idn,
|
|
230
|
+
integration_idn: integrationIdn,
|
|
231
|
+
settings: connectorData.settings
|
|
232
|
+
});
|
|
233
|
+
createdCount++;
|
|
234
|
+
if (verbose)
|
|
235
|
+
console.log(` ā
Created: ${connectorData.title}`);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
console.error(` ā Failed to create connector: ${error.message}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
// Check if connector needs update
|
|
243
|
+
const needsUpdate = hasConnectorChanged(remoteConnector, connectorData);
|
|
244
|
+
if (needsUpdate) {
|
|
245
|
+
if (verbose)
|
|
246
|
+
console.log(` š Updating connector: ${connectorData.title}`);
|
|
247
|
+
try {
|
|
248
|
+
await updateConnector(client, remoteConnector.id, {
|
|
249
|
+
title: connectorData.title,
|
|
250
|
+
status: connectorData.status,
|
|
251
|
+
settings: connectorData.settings
|
|
252
|
+
});
|
|
253
|
+
updatedCount++;
|
|
254
|
+
if (verbose)
|
|
255
|
+
console.log(` ā
Updated: ${connectorData.title}`);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
console.error(` ā Failed to update connector: ${error.message}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
if (verbose)
|
|
263
|
+
console.log(` ā No changes: ${connectorData.title}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Delete connectors that exist remotely but not locally
|
|
268
|
+
for (const [connectorIdn, remoteConnector] of remoteConnectorMap) {
|
|
269
|
+
if (!localConnectorIdns.has(connectorIdn)) {
|
|
270
|
+
if (verbose)
|
|
271
|
+
console.log(` šļø Deleting connector: ${remoteConnector.title}`);
|
|
272
|
+
try {
|
|
273
|
+
await deleteConnector(client, remoteConnector.id);
|
|
274
|
+
deletedCount++;
|
|
275
|
+
if (verbose)
|
|
276
|
+
console.log(` ā
Deleted: ${remoteConnector.title}`);
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
console.error(` ā Failed to delete connector: ${error.message}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Always show summary if changes were made, not just in verbose mode
|
|
286
|
+
if (createdCount > 0 || updatedCount > 0 || deletedCount > 0) {
|
|
287
|
+
console.log(`\nā
Integration push completed:`);
|
|
288
|
+
console.log(` Created: ${createdCount} connector(s)`);
|
|
289
|
+
console.log(` Updated: ${updatedCount} connector(s)`);
|
|
290
|
+
console.log(` Deleted: ${deletedCount} connector(s)`);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
console.log(`\nā No connector changes to push`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Check if connector has changed compared to remote version
|
|
298
|
+
*/
|
|
299
|
+
function hasConnectorChanged(remote, local) {
|
|
300
|
+
// Check title
|
|
301
|
+
if (remote.title !== local.title)
|
|
302
|
+
return true;
|
|
303
|
+
// Check status
|
|
304
|
+
if (remote.status !== local.status)
|
|
305
|
+
return true;
|
|
306
|
+
// Check settings
|
|
307
|
+
if (remote.settings.length !== local.settings.length)
|
|
308
|
+
return true;
|
|
309
|
+
// Compare each setting
|
|
310
|
+
const remoteSettingsMap = new Map();
|
|
311
|
+
remote.settings.forEach(s => remoteSettingsMap.set(s.idn, s.value));
|
|
312
|
+
for (const localSetting of local.settings) {
|
|
313
|
+
const remoteValue = remoteSettingsMap.get(localSetting.idn);
|
|
314
|
+
if (remoteValue !== localSetting.value)
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* List all local integrations from file system
|
|
321
|
+
*/
|
|
322
|
+
export async function listLocalIntegrations(customerDir) {
|
|
323
|
+
const integrationsFile = path.join(customerDir, 'integrations', 'integrations.yaml');
|
|
324
|
+
if (!await fs.pathExists(integrationsFile)) {
|
|
325
|
+
return [];
|
|
326
|
+
}
|
|
327
|
+
const data = yaml.load(await fs.readFile(integrationsFile, 'utf-8'));
|
|
328
|
+
return data.integrations;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Get connector details from local file
|
|
332
|
+
*/
|
|
333
|
+
export async function getLocalConnector(customerDir, integrationIdn, connectorIdn) {
|
|
334
|
+
const connectorFile = path.join(customerDir, 'integrations', integrationIdn, 'connectors', connectorIdn, `${connectorIdn}.yaml`);
|
|
335
|
+
if (!await fs.pathExists(connectorFile)) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
return yaml.load(await fs.readFile(connectorFile, 'utf-8'));
|
|
339
|
+
}
|
|
340
|
+
//# sourceMappingURL=integrations.js.map
|
package/dist/sync/projects.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Project synchronization operations
|
|
3
3
|
*/
|
|
4
4
|
import { listProjects, listAgents, listFlowSkills, listFlowEvents, listFlowStates } from '../api.js';
|
|
5
|
-
import { ensureState, writeFileSafe, mapPath, projectMetadataPath, agentMetadataPath, flowMetadataPath, skillMetadataPath, skillScriptPath, skillFolderPath, flowsYamlPath, customerAttributesPath } from '../fsutil.js';
|
|
5
|
+
import { ensureState, writeFileSafe, mapPath, projectMetadataPath, agentMetadataPath, flowMetadataPath, skillMetadataPath, skillScriptPath, skillFolderPath, flowsYamlPath, customerAttributesPath, customerProjectsDir, projectDir } from '../fsutil.js';
|
|
6
6
|
import { findSkillScriptFiles, isContentDifferent, askForOverwrite, getExtensionForRunner } from './skill-files.js';
|
|
7
7
|
import fs from 'fs-extra';
|
|
8
8
|
import { sha256, saveHashes } from '../hash.js';
|
|
@@ -16,6 +16,174 @@ export function isProjectMap(x) {
|
|
|
16
16
|
export function isLegacyProjectMap(x) {
|
|
17
17
|
return typeof x === 'object' && x !== null && 'projectId' in x && 'agents' in x;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Ask user for deletion confirmation
|
|
21
|
+
*/
|
|
22
|
+
async function askForDeletion(entityType, entityPath) {
|
|
23
|
+
const readline = await import('readline');
|
|
24
|
+
const rl = readline.createInterface({
|
|
25
|
+
input: process.stdin,
|
|
26
|
+
output: process.stdout
|
|
27
|
+
});
|
|
28
|
+
const answer = await new Promise((resolve) => {
|
|
29
|
+
rl.question(`\nšļø Delete ${entityType}: ${entityPath}? (y)es/(n)o/(a)ll/(q)uit: `, resolve);
|
|
30
|
+
});
|
|
31
|
+
rl.close();
|
|
32
|
+
const choice = answer.toLowerCase().trim();
|
|
33
|
+
if (choice === 'q' || choice === 'quit') {
|
|
34
|
+
return 'quit';
|
|
35
|
+
}
|
|
36
|
+
if (choice === 'a' || choice === 'all') {
|
|
37
|
+
return 'all';
|
|
38
|
+
}
|
|
39
|
+
if (choice === 'y' || choice === 'yes') {
|
|
40
|
+
return 'yes';
|
|
41
|
+
}
|
|
42
|
+
return 'no';
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Clean up deleted entities (projects, agents, flows, skills) that no longer exist remotely
|
|
46
|
+
*/
|
|
47
|
+
async function cleanupDeletedEntities(customerIdn, projectMap, verbose = false) {
|
|
48
|
+
const projectsDir = customerProjectsDir(customerIdn);
|
|
49
|
+
if (!(await fs.pathExists(projectsDir))) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const deletedEntities = [];
|
|
53
|
+
// Scan local filesystem for entities
|
|
54
|
+
const localProjects = await fs.readdir(projectsDir);
|
|
55
|
+
for (const projectIdn of localProjects) {
|
|
56
|
+
const projectPath = projectDir(customerIdn, projectIdn);
|
|
57
|
+
const projectStat = await fs.stat(projectPath).catch(() => null);
|
|
58
|
+
// Skip files
|
|
59
|
+
if (!projectStat || !projectStat.isDirectory())
|
|
60
|
+
continue;
|
|
61
|
+
// Skip flows.yaml
|
|
62
|
+
if (projectIdn === 'flows.yaml')
|
|
63
|
+
continue;
|
|
64
|
+
// Check if project exists in map
|
|
65
|
+
const projectData = projectMap.projects[projectIdn];
|
|
66
|
+
if (!projectData) {
|
|
67
|
+
// Entire project was deleted remotely
|
|
68
|
+
deletedEntities.push({
|
|
69
|
+
type: 'project',
|
|
70
|
+
path: projectPath,
|
|
71
|
+
displayPath: projectIdn
|
|
72
|
+
});
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
// Scan for agents within this project
|
|
76
|
+
try {
|
|
77
|
+
const localAgents = await fs.readdir(projectPath);
|
|
78
|
+
for (const agentIdn of localAgents) {
|
|
79
|
+
const agentPath = `${projectPath}/${agentIdn}`;
|
|
80
|
+
const agentStat = await fs.stat(agentPath).catch(() => null);
|
|
81
|
+
// Skip files and metadata.yaml
|
|
82
|
+
if (!agentStat || !agentStat.isDirectory())
|
|
83
|
+
continue;
|
|
84
|
+
// Check if agent exists in map
|
|
85
|
+
const agentData = projectData.agents[agentIdn];
|
|
86
|
+
if (!agentData) {
|
|
87
|
+
// Agent was deleted remotely
|
|
88
|
+
deletedEntities.push({
|
|
89
|
+
type: 'agent',
|
|
90
|
+
path: agentPath,
|
|
91
|
+
displayPath: `${projectIdn}/${agentIdn}`
|
|
92
|
+
});
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
// Scan for flows within this agent
|
|
96
|
+
try {
|
|
97
|
+
const localFlows = await fs.readdir(agentPath);
|
|
98
|
+
for (const flowIdn of localFlows) {
|
|
99
|
+
const flowPath = `${agentPath}/${flowIdn}`;
|
|
100
|
+
const flowStat = await fs.stat(flowPath).catch(() => null);
|
|
101
|
+
// Skip files and metadata.yaml
|
|
102
|
+
if (!flowStat || !flowStat.isDirectory())
|
|
103
|
+
continue;
|
|
104
|
+
// Check if flow exists in map
|
|
105
|
+
const flowData = agentData.flows[flowIdn];
|
|
106
|
+
if (!flowData) {
|
|
107
|
+
// Flow was deleted remotely
|
|
108
|
+
deletedEntities.push({
|
|
109
|
+
type: 'flow',
|
|
110
|
+
path: flowPath,
|
|
111
|
+
displayPath: `${projectIdn}/${agentIdn}/${flowIdn}`
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
// Scan for skills within this flow
|
|
116
|
+
try {
|
|
117
|
+
const localSkills = await fs.readdir(flowPath);
|
|
118
|
+
for (const skillIdn of localSkills) {
|
|
119
|
+
const skillPath = `${flowPath}/${skillIdn}`;
|
|
120
|
+
const skillStat = await fs.stat(skillPath).catch(() => null);
|
|
121
|
+
// Skip files and metadata.yaml
|
|
122
|
+
if (!skillStat || !skillStat.isDirectory())
|
|
123
|
+
continue;
|
|
124
|
+
// Check if skill exists in map
|
|
125
|
+
const skillData = flowData.skills[skillIdn];
|
|
126
|
+
if (!skillData) {
|
|
127
|
+
// Skill was deleted remotely
|
|
128
|
+
deletedEntities.push({
|
|
129
|
+
type: 'skill',
|
|
130
|
+
path: skillPath,
|
|
131
|
+
displayPath: `${projectIdn}/${agentIdn}/${flowIdn}/${skillIdn}`
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
// Ignore errors reading flow directory
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
// Ignore errors reading agent directory
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
// Ignore errors reading project directory
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (deletedEntities.length === 0) {
|
|
151
|
+
if (verbose)
|
|
152
|
+
console.log('ā
No deleted entities found');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
console.log(`\nš Found ${deletedEntities.length} entity(ies) that no longer exist remotely:`);
|
|
156
|
+
for (const entity of deletedEntities) {
|
|
157
|
+
console.log(` ${entity.type.padEnd(8)}: ${entity.displayPath}`);
|
|
158
|
+
}
|
|
159
|
+
console.log('\nThese entities will be deleted from your local filesystem.');
|
|
160
|
+
let globalDeleteAll = false;
|
|
161
|
+
for (const entity of deletedEntities) {
|
|
162
|
+
let shouldDelete = globalDeleteAll;
|
|
163
|
+
if (!globalDeleteAll) {
|
|
164
|
+
const choice = await askForDeletion(entity.type, entity.displayPath);
|
|
165
|
+
if (choice === 'quit') {
|
|
166
|
+
console.log('ā Deletion cancelled by user');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
else if (choice === 'all') {
|
|
170
|
+
globalDeleteAll = true;
|
|
171
|
+
shouldDelete = true;
|
|
172
|
+
}
|
|
173
|
+
else if (choice === 'yes') {
|
|
174
|
+
shouldDelete = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (shouldDelete) {
|
|
178
|
+
await fs.remove(entity.path);
|
|
179
|
+
console.log(`šļø Deleted: ${entity.displayPath}`);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
console.log(`āļø Skipped: ${entity.displayPath}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
console.log(`\nā
Cleanup completed`);
|
|
186
|
+
}
|
|
19
187
|
/**
|
|
20
188
|
* Pull a single project and all its data
|
|
21
189
|
*/
|
|
@@ -284,6 +452,8 @@ export async function pullSingleProject(client, customer, projectId, verbose = f
|
|
|
284
452
|
newHashes[flowsYamlFilePath] = sha256(flowsYamlContent);
|
|
285
453
|
// Save hashes (now including flows.yaml and attributes.yaml)
|
|
286
454
|
await saveHashes(newHashes, customer.idn);
|
|
455
|
+
// Detect and clean up deleted entities
|
|
456
|
+
await cleanupDeletedEntities(customer.idn, existingMap, verbose);
|
|
287
457
|
}
|
|
288
458
|
/**
|
|
289
459
|
* Pull all projects for a customer
|
package/dist/sync/push.js
CHANGED
|
@@ -10,6 +10,7 @@ import yaml from 'js-yaml';
|
|
|
10
10
|
import { generateFlowsYaml } from './metadata.js';
|
|
11
11
|
import { isProjectMap, isLegacyProjectMap } from './projects.js';
|
|
12
12
|
import { flowsYamlPath } from '../fsutil.js';
|
|
13
|
+
import { pushAllProjectAttributes } from './attributes.js';
|
|
13
14
|
/**
|
|
14
15
|
* Scan filesystem for local-only entities not in the project map yet
|
|
15
16
|
*/
|
|
@@ -445,6 +446,20 @@ export async function pushChanged(client, customer, verbose = false, shouldPubli
|
|
|
445
446
|
}
|
|
446
447
|
if (verbose)
|
|
447
448
|
console.log(`š Scanned ${scanned} files, found ${pushed} changes`);
|
|
449
|
+
// Push project attributes for all projects
|
|
450
|
+
const projectsInfoMap = {};
|
|
451
|
+
for (const [projectIdn, projectData] of Object.entries(projects)) {
|
|
452
|
+
if (projectIdn && projectData.projectId) {
|
|
453
|
+
projectsInfoMap[projectIdn] = {
|
|
454
|
+
projectId: projectData.projectId,
|
|
455
|
+
projectIdn: projectData.projectIdn || projectIdn
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const attributesUpdated = await pushAllProjectAttributes(client, customer, projectsInfoMap, verbose);
|
|
460
|
+
if (attributesUpdated > 0) {
|
|
461
|
+
pushed += attributesUpdated;
|
|
462
|
+
}
|
|
448
463
|
// Regenerate flows.yaml if metadata was changed
|
|
449
464
|
if (metadataChanged) {
|
|
450
465
|
if (verbose)
|
package/dist/sync/skill-files.js
CHANGED
|
@@ -63,7 +63,7 @@ export async function validateSkillFolder(customerIdn, projectIdn, agentIdn, flo
|
|
|
63
63
|
const warnings = [];
|
|
64
64
|
const errors = [];
|
|
65
65
|
if (files.length === 0) {
|
|
66
|
-
errors.push(`No script files found in skill folder: ${
|
|
66
|
+
errors.push(`No script files found in skill folder: ${folderPath}`);
|
|
67
67
|
}
|
|
68
68
|
else if (files.length > 1) {
|
|
69
69
|
errors.push(`Multiple script files found in skill ${skillIdn}: ${files.map(f => f.fileName).join(', ')}`);
|
package/dist/sync/status.js
CHANGED
|
@@ -238,7 +238,8 @@ export async function status(customer, verbose = false) {
|
|
|
238
238
|
console.warn(` Status check skipped - please keep only one script file.`);
|
|
239
239
|
}
|
|
240
240
|
else if (validation.files.length === 0) {
|
|
241
|
-
|
|
241
|
+
const displayPath = projectIdn ? `${projectIdn}/${agentIdn}/${flowIdn}/${skillIdn}` : `${agentIdn}/${flowIdn}/${skillIdn}`;
|
|
242
|
+
console.log(`D ${displayPath}/ (no script files)`);
|
|
242
243
|
dirty++;
|
|
243
244
|
}
|
|
244
245
|
continue;
|
|
@@ -246,7 +247,8 @@ export async function status(customer, verbose = false) {
|
|
|
246
247
|
// Get the single valid script file
|
|
247
248
|
const skillFile = await getSingleSkillFile(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn);
|
|
248
249
|
if (!skillFile) {
|
|
249
|
-
|
|
250
|
+
const displayPath = projectIdn ? `${projectIdn}/${agentIdn}/${flowIdn}/${skillIdn}` : `${agentIdn}/${flowIdn}/${skillIdn}`;
|
|
251
|
+
console.log(`D ${displayPath}/ (no valid script file)`);
|
|
250
252
|
dirty++;
|
|
251
253
|
if (verbose)
|
|
252
254
|
console.log(` ā No valid script file found: ${skillIdn}`);
|