newo 1.7.3 → 1.8.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 +25 -0
- package/dist/fsutil.d.ts +1 -0
- package/dist/fsutil.js +3 -0
- package/dist/sync.js +346 -26
- package/package.json +2 -2
- package/src/fsutil.ts +4 -0
- package/src/sync.ts +385 -27
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.8.0] - 2025-09-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Complete Metadata Change Tracking**: Comprehensive metadata.yaml file synchronization
|
|
12
|
+
- All metadata.yaml files now tracked with hash-based change detection
|
|
13
|
+
- Status command shows detailed metadata changes (title, runner_type, model)
|
|
14
|
+
- Push command automatically updates skills when metadata changes
|
|
15
|
+
- flows.yaml automatically regenerated when metadata changes detected
|
|
16
|
+
- Preserves flows.yaml format consistency with backup/comparison system
|
|
17
|
+
|
|
18
|
+
### Enhanced
|
|
19
|
+
- **Comprehensive File Synchronization**: All NEWO workspace files fully tracked
|
|
20
|
+
- Skills: .guidance and .jinja script files with hash tracking ✓
|
|
21
|
+
- Metadata: metadata.yaml files with skill updates + flows.yaml regeneration ✓
|
|
22
|
+
- Attributes: attributes.yaml with diff-based sync for 233 customer attributes ✓
|
|
23
|
+
- Flows: flows.yaml with automatic regeneration and format preservation ✓
|
|
24
|
+
- Multi-customer: All file types synchronized across multiple customer workspaces ✓
|
|
25
|
+
|
|
26
|
+
### Technical
|
|
27
|
+
- **flows.yaml Regeneration**: Automatic regeneration pipeline when metadata changes
|
|
28
|
+
- Creates backup before regeneration for format comparison
|
|
29
|
+
- Re-fetches project data to ensure accuracy
|
|
30
|
+
- Updates hash tracking for regenerated flows.yaml
|
|
31
|
+
- Maintains consistent YAML format structure
|
|
32
|
+
|
|
8
33
|
## [1.7.3] - 2025-09-15
|
|
9
34
|
|
|
10
35
|
### Added
|
package/dist/fsutil.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare function projectDir(customerIdn: string, projectIdn: string): str
|
|
|
11
11
|
export declare function flowsYamlPath(customerIdn: string): string;
|
|
12
12
|
export declare function customerAttributesPath(customerIdn: string): string;
|
|
13
13
|
export declare function customerAttributesMapPath(customerIdn: string): string;
|
|
14
|
+
export declare function customerAttributesBackupPath(customerIdn: string): string;
|
|
14
15
|
export declare function skillPath(customerIdn: string, projectIdn: string, agentIdn: string, flowIdn: string, skillIdn: string, runnerType?: RunnerType): string;
|
|
15
16
|
export declare function skillFolderPath(customerIdn: string, projectIdn: string, agentIdn: string, flowIdn: string, skillIdn: string): string;
|
|
16
17
|
export declare function skillScriptPath(customerIdn: string, projectIdn: string, agentIdn: string, flowIdn: string, skillIdn: string, runnerType?: RunnerType): string;
|
package/dist/fsutil.js
CHANGED
|
@@ -34,6 +34,9 @@ export function customerAttributesPath(customerIdn) {
|
|
|
34
34
|
export function customerAttributesMapPath(customerIdn) {
|
|
35
35
|
return path.join(customerStateDir(customerIdn), 'attributes-map.json');
|
|
36
36
|
}
|
|
37
|
+
export function customerAttributesBackupPath(customerIdn) {
|
|
38
|
+
return path.join(customerStateDir(customerIdn), 'attributes-backup.yaml');
|
|
39
|
+
}
|
|
37
40
|
// Legacy skill path - direct file
|
|
38
41
|
export function skillPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn, runnerType = 'guidance') {
|
|
39
42
|
const extension = runnerType === 'nsl' ? '.jinja' : '.guidance';
|
package/dist/sync.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { listProjects, listAgents, listFlowSkills, updateSkill, listFlowEvents, listFlowStates, getProjectMeta, getCustomerAttributes } from './api.js';
|
|
2
|
-
import { ensureState, skillPath, skillScriptPath, writeFileSafe, readIfExists, mapPath, projectMetadataPath, agentMetadataPath, flowMetadataPath, skillMetadataPath, flowsYamlPath, customerAttributesPath, customerAttributesMapPath } from './fsutil.js';
|
|
1
|
+
import { listProjects, listAgents, listFlowSkills, updateSkill, listFlowEvents, listFlowStates, getProjectMeta, getCustomerAttributes, updateCustomerAttribute } from './api.js';
|
|
2
|
+
import { ensureState, skillPath, skillScriptPath, writeFileSafe, readIfExists, mapPath, projectMetadataPath, agentMetadataPath, flowMetadataPath, skillMetadataPath, flowsYamlPath, customerAttributesPath, customerAttributesMapPath, customerAttributesBackupPath } from './fsutil.js';
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import { sha256, loadHashes, saveHashes } from './hash.js';
|
|
5
5
|
import yaml from 'js-yaml';
|
|
@@ -76,12 +76,14 @@ export async function saveCustomerAttributes(client, customer, verbose = false)
|
|
|
76
76
|
yamlContent = yamlContent.replace(/__ENUM_PLACEHOLDER_(\w+)__/g, '!enum "AttributeValueTypes.$1"');
|
|
77
77
|
// Fix JSON string formatting to match reference (remove escape characters)
|
|
78
78
|
yamlContent = yamlContent.replace(/\\"/g, '"');
|
|
79
|
-
// Save
|
|
79
|
+
// Save all files: attributes.yaml, ID mapping, and backup for diff tracking
|
|
80
80
|
await writeFileSafe(customerAttributesPath(customer.idn), yamlContent);
|
|
81
81
|
await writeFileSafe(customerAttributesMapPath(customer.idn), JSON.stringify(idMapping, null, 2));
|
|
82
|
+
await writeFileSafe(customerAttributesBackupPath(customer.idn), yamlContent);
|
|
82
83
|
if (verbose) {
|
|
83
84
|
console.log(`✓ Saved customer attributes to ${customerAttributesPath(customer.idn)}`);
|
|
84
85
|
console.log(`✓ Saved attribute ID mapping to ${customerAttributesMapPath(customer.idn)}`);
|
|
86
|
+
console.log(`✓ Created attributes backup for diff tracking`);
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
catch (error) {
|
|
@@ -215,6 +217,12 @@ export async function pullAll(client, customer, projectId = null, verbose = fals
|
|
|
215
217
|
const newPath = skillScriptPath(customer.idn, projectMeta.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
216
218
|
const content = await fs.readFile(newPath, 'utf8');
|
|
217
219
|
hashes[newPath] = sha256(content);
|
|
220
|
+
// Track skill metadata.yaml file
|
|
221
|
+
const metadataPath = skillMetadataPath(customer.idn, projectMeta.idn, agentIdn, flowIdn, skillIdn);
|
|
222
|
+
if (await fs.pathExists(metadataPath)) {
|
|
223
|
+
const metadataContent = await fs.readFile(metadataPath, 'utf8');
|
|
224
|
+
hashes[metadataPath] = sha256(metadataContent);
|
|
225
|
+
}
|
|
218
226
|
// Also track legacy path for backwards compatibility during transition
|
|
219
227
|
const legacyPath = skillPath(customer.idn, projectMeta.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
220
228
|
hashes[legacyPath] = sha256(content);
|
|
@@ -232,6 +240,14 @@ export async function pullAll(client, customer, projectId = null, verbose = fals
|
|
|
232
240
|
if (verbose)
|
|
233
241
|
console.log(`✓ Added attributes.yaml to hash tracking`);
|
|
234
242
|
}
|
|
243
|
+
// Add flows.yaml to hash tracking
|
|
244
|
+
const flowsFile = flowsYamlPath(customer.idn);
|
|
245
|
+
if (await fs.pathExists(flowsFile)) {
|
|
246
|
+
const flowsContent = await fs.readFile(flowsFile, 'utf8');
|
|
247
|
+
hashes[flowsFile] = sha256(flowsContent);
|
|
248
|
+
if (verbose)
|
|
249
|
+
console.log(`✓ Added flows.yaml to hash tracking`);
|
|
250
|
+
}
|
|
235
251
|
}
|
|
236
252
|
catch (error) {
|
|
237
253
|
console.error(`❌ Failed to save customer attributes for ${customer.idn}:`, error);
|
|
@@ -261,6 +277,12 @@ export async function pullAll(client, customer, projectId = null, verbose = fals
|
|
|
261
277
|
const newPath = skillScriptPath(customer.idn, project.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
262
278
|
const content = await fs.readFile(newPath, 'utf8');
|
|
263
279
|
allHashes[newPath] = sha256(content);
|
|
280
|
+
// Track skill metadata.yaml file
|
|
281
|
+
const metadataPath = skillMetadataPath(customer.idn, project.idn, agentIdn, flowIdn, skillIdn);
|
|
282
|
+
if (await fs.pathExists(metadataPath)) {
|
|
283
|
+
const metadataContent = await fs.readFile(metadataPath, 'utf8');
|
|
284
|
+
allHashes[metadataPath] = sha256(metadataContent);
|
|
285
|
+
}
|
|
264
286
|
// Also track legacy path for backwards compatibility during transition
|
|
265
287
|
const legacyPath = skillPath(customer.idn, project.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
266
288
|
allHashes[legacyPath] = sha256(content);
|
|
@@ -280,6 +302,14 @@ export async function pullAll(client, customer, projectId = null, verbose = fals
|
|
|
280
302
|
if (verbose)
|
|
281
303
|
console.log(`✓ Added attributes.yaml to hash tracking`);
|
|
282
304
|
}
|
|
305
|
+
// Add flows.yaml to hash tracking
|
|
306
|
+
const flowsFile = flowsYamlPath(customer.idn);
|
|
307
|
+
if (await fs.pathExists(flowsFile)) {
|
|
308
|
+
const flowsContent = await fs.readFile(flowsFile, 'utf8');
|
|
309
|
+
allHashes[flowsFile] = sha256(flowsContent);
|
|
310
|
+
if (verbose)
|
|
311
|
+
console.log(`✓ Added flows.yaml to hash tracking`);
|
|
312
|
+
}
|
|
283
313
|
}
|
|
284
314
|
catch (error) {
|
|
285
315
|
console.error(`❌ Failed to save customer attributes for ${customer.idn}:`, error);
|
|
@@ -303,6 +333,7 @@ export async function pushChanged(client, customer, verbose = false) {
|
|
|
303
333
|
console.log('🔄 Scanning for changes...');
|
|
304
334
|
let pushed = 0;
|
|
305
335
|
let scanned = 0;
|
|
336
|
+
let metadataChanged = false;
|
|
306
337
|
// Handle both old single-project format and new multi-project format with type guards
|
|
307
338
|
const projects = isProjectMap(idMapData) && idMapData.projects
|
|
308
339
|
? idMapData.projects
|
|
@@ -403,37 +434,178 @@ export async function pushChanged(client, customer, verbose = false) {
|
|
|
403
434
|
}
|
|
404
435
|
}
|
|
405
436
|
}
|
|
437
|
+
// Check for metadata-only changes (when metadata changed but script didn't)
|
|
438
|
+
try {
|
|
439
|
+
for (const [projectIdn, projectData] of Object.entries(projects)) {
|
|
440
|
+
for (const [agentIdn, agentObj] of Object.entries(projectData.agents)) {
|
|
441
|
+
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
442
|
+
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
443
|
+
const metadataPath = projectIdn ?
|
|
444
|
+
skillMetadataPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn) :
|
|
445
|
+
skillMetadataPath(customer.idn, '', agentIdn, flowIdn, skillIdn);
|
|
446
|
+
if (await fs.pathExists(metadataPath)) {
|
|
447
|
+
const metadataContent = await fs.readFile(metadataPath, 'utf8');
|
|
448
|
+
const h = sha256(metadataContent);
|
|
449
|
+
const oldHash = oldHashes[metadataPath];
|
|
450
|
+
if (oldHash !== h) {
|
|
451
|
+
if (verbose)
|
|
452
|
+
console.log(`🔄 Metadata-only change detected for ${skillIdn}, updating skill...`);
|
|
453
|
+
try {
|
|
454
|
+
// Load updated metadata
|
|
455
|
+
const updatedMetadata = yaml.load(metadataContent);
|
|
456
|
+
// Get current script content
|
|
457
|
+
const scriptPath = projectIdn ?
|
|
458
|
+
skillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
|
|
459
|
+
skillScriptPath(customer.idn, '', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
460
|
+
let scriptContent = '';
|
|
461
|
+
if (await fs.pathExists(scriptPath)) {
|
|
462
|
+
scriptContent = await fs.readFile(scriptPath, 'utf8');
|
|
463
|
+
}
|
|
464
|
+
// Create skill object with updated metadata
|
|
465
|
+
const skillObject = {
|
|
466
|
+
id: updatedMetadata.id,
|
|
467
|
+
title: updatedMetadata.title,
|
|
468
|
+
idn: updatedMetadata.idn,
|
|
469
|
+
prompt_script: scriptContent,
|
|
470
|
+
runner_type: updatedMetadata.runner_type,
|
|
471
|
+
model: updatedMetadata.model,
|
|
472
|
+
parameters: updatedMetadata.parameters,
|
|
473
|
+
path: updatedMetadata.path || undefined
|
|
474
|
+
};
|
|
475
|
+
await updateSkill(client, skillObject);
|
|
476
|
+
console.log(`↑ Pushed metadata update for skill: ${skillIdn} (${updatedMetadata.title})`);
|
|
477
|
+
newHashes[metadataPath] = h;
|
|
478
|
+
pushed++;
|
|
479
|
+
metadataChanged = true;
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
console.error(`❌ Failed to push metadata for ${skillIdn}: ${error instanceof Error ? error.message : String(error)}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
if (verbose)
|
|
493
|
+
console.log(`⚠️ Metadata push check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
494
|
+
}
|
|
406
495
|
if (verbose)
|
|
407
496
|
console.log(`🔄 Scanned ${scanned} files, found ${pushed} changes`);
|
|
408
|
-
// Check for attributes changes and push
|
|
497
|
+
// Check for attributes changes and push specific changed attributes only
|
|
409
498
|
try {
|
|
410
499
|
const attributesFile = customerAttributesPath(customer.idn);
|
|
411
500
|
const attributesMapFile = customerAttributesMapPath(customer.idn);
|
|
501
|
+
const attributesBackupFile = customerAttributesBackupPath(customer.idn);
|
|
412
502
|
if (await fs.pathExists(attributesFile) && await fs.pathExists(attributesMapFile)) {
|
|
413
503
|
if (verbose)
|
|
414
504
|
console.log('🔍 Checking customer attributes for changes...');
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
505
|
+
const currentContent = await fs.readFile(attributesFile, 'utf8');
|
|
506
|
+
// Check if backup exists for diff comparison
|
|
507
|
+
if (await fs.pathExists(attributesBackupFile)) {
|
|
508
|
+
const backupContent = await fs.readFile(attributesBackupFile, 'utf8');
|
|
509
|
+
if (currentContent !== backupContent) {
|
|
510
|
+
if (verbose)
|
|
511
|
+
console.log(`🔄 Attributes file changed, analyzing differences...`);
|
|
512
|
+
try {
|
|
513
|
+
// Load ID mapping for push operations
|
|
514
|
+
const idMapping = await fs.readJson(attributesMapFile);
|
|
515
|
+
// Parse both versions to find changed attributes
|
|
516
|
+
const parseYaml = (content) => {
|
|
517
|
+
let yamlContent = content.replace(/!enum "([^"]+)"/g, '"$1"');
|
|
518
|
+
return yaml.load(yamlContent);
|
|
519
|
+
};
|
|
520
|
+
const currentData = parseYaml(currentContent);
|
|
521
|
+
const backupData = parseYaml(backupContent);
|
|
522
|
+
if (currentData?.attributes && backupData?.attributes) {
|
|
523
|
+
// Create maps for comparison
|
|
524
|
+
const currentAttrs = new Map(currentData.attributes.map(attr => [attr.idn, attr]));
|
|
525
|
+
const backupAttrs = new Map(backupData.attributes.map(attr => [attr.idn, attr]));
|
|
526
|
+
let attributesPushed = 0;
|
|
527
|
+
// Find changed attributes
|
|
528
|
+
for (const [idn, currentAttr] of currentAttrs) {
|
|
529
|
+
const backupAttr = backupAttrs.get(idn);
|
|
530
|
+
// Check if attribute changed (deep comparison of key fields)
|
|
531
|
+
const hasChanged = !backupAttr ||
|
|
532
|
+
currentAttr.value !== backupAttr.value ||
|
|
533
|
+
currentAttr.title !== backupAttr.title ||
|
|
534
|
+
currentAttr.description !== backupAttr.description ||
|
|
535
|
+
currentAttr.group !== backupAttr.group ||
|
|
536
|
+
currentAttr.is_hidden !== backupAttr.is_hidden;
|
|
537
|
+
if (hasChanged) {
|
|
538
|
+
const attributeId = idMapping[idn];
|
|
539
|
+
if (!attributeId) {
|
|
540
|
+
if (verbose)
|
|
541
|
+
console.log(`⚠️ Skipping ${idn} - no ID mapping`);
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
// Create attribute object for push
|
|
545
|
+
const attributeToUpdate = {
|
|
546
|
+
id: attributeId,
|
|
547
|
+
idn: currentAttr.idn,
|
|
548
|
+
value: currentAttr.value,
|
|
549
|
+
title: currentAttr.title || "",
|
|
550
|
+
description: currentAttr.description || "",
|
|
551
|
+
group: currentAttr.group || "",
|
|
552
|
+
is_hidden: currentAttr.is_hidden,
|
|
553
|
+
possible_values: currentAttr.possible_values || [],
|
|
554
|
+
value_type: currentAttr.value_type?.replace(/^"?AttributeValueTypes\.(.+)"?$/, '$1') || "string"
|
|
555
|
+
};
|
|
556
|
+
await updateCustomerAttribute(client, attributeToUpdate);
|
|
557
|
+
attributesPushed++;
|
|
558
|
+
if (verbose) {
|
|
559
|
+
console.log(` ✓ Pushed changed attribute: ${idn}`);
|
|
560
|
+
console.log(` Old value: ${backupAttr?.value || 'N/A'}`);
|
|
561
|
+
console.log(` New value: ${currentAttr.value}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (attributesPushed > 0) {
|
|
566
|
+
console.log(`↑ Pushed ${attributesPushed} changed customer attributes to NEWO API`);
|
|
567
|
+
// Show summary of what was pushed
|
|
568
|
+
console.log(` 📊 Pushed attributes:`);
|
|
569
|
+
for (const [idn, currentAttr] of currentAttrs) {
|
|
570
|
+
const backupAttr = backupAttrs.get(idn);
|
|
571
|
+
const hasChanged = !backupAttr ||
|
|
572
|
+
currentAttr.value !== backupAttr.value ||
|
|
573
|
+
currentAttr.title !== backupAttr.title ||
|
|
574
|
+
currentAttr.description !== backupAttr.description ||
|
|
575
|
+
currentAttr.group !== backupAttr.group ||
|
|
576
|
+
currentAttr.is_hidden !== backupAttr.is_hidden;
|
|
577
|
+
if (hasChanged) {
|
|
578
|
+
console.log(` • ${idn}: ${currentAttr.title || 'No title'}`);
|
|
579
|
+
console.log(` Value: ${currentAttr.value}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
// Update backup file after successful push
|
|
583
|
+
await fs.writeFile(attributesBackupFile, currentContent, 'utf8');
|
|
584
|
+
newHashes[attributesFile] = sha256(currentContent);
|
|
585
|
+
pushed++;
|
|
586
|
+
}
|
|
587
|
+
else if (verbose) {
|
|
588
|
+
console.log(` ✓ No attribute value changes detected`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
console.log(`⚠️ Failed to parse attributes for comparison`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
catch (error) {
|
|
596
|
+
console.error(`❌ Failed to push changed attributes: ${error instanceof Error ? error.message : String(error)}`);
|
|
597
|
+
// Don't update hash/backup on failure so it will retry next time
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
else if (verbose) {
|
|
601
|
+
console.log(` ✓ No attributes file changes`);
|
|
602
|
+
}
|
|
423
603
|
}
|
|
424
|
-
|
|
604
|
+
else {
|
|
605
|
+
// No backup exists, create initial backup
|
|
606
|
+
await fs.writeFile(attributesBackupFile, currentContent, 'utf8');
|
|
425
607
|
if (verbose)
|
|
426
|
-
console.log(
|
|
427
|
-
// TODO: Implement actual attributes push here
|
|
428
|
-
// For now, just update the hash to mark as "pushed"
|
|
429
|
-
console.log(`↑ Attributes changed: ${attributesFile}`);
|
|
430
|
-
newHashes[attributesFile] = h;
|
|
431
|
-
pushed++;
|
|
432
|
-
// Note: Individual attribute push would require parsing the YAML and comparing specific attributes
|
|
433
|
-
// This is a placeholder for future implementation
|
|
434
|
-
}
|
|
435
|
-
else if (verbose) {
|
|
436
|
-
console.log(` ✓ No attributes changes`);
|
|
608
|
+
console.log(`✓ Created initial attributes backup for diff tracking`);
|
|
437
609
|
}
|
|
438
610
|
}
|
|
439
611
|
else if (verbose) {
|
|
@@ -444,6 +616,47 @@ export async function pushChanged(client, customer, verbose = false) {
|
|
|
444
616
|
if (verbose)
|
|
445
617
|
console.log(`⚠️ Attributes push check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
446
618
|
}
|
|
619
|
+
// Regenerate flows.yaml if metadata changed
|
|
620
|
+
if (metadataChanged) {
|
|
621
|
+
try {
|
|
622
|
+
if (verbose)
|
|
623
|
+
console.log('🔄 Metadata changed, regenerating flows.yaml...');
|
|
624
|
+
// Create backup of current flows.yaml for format comparison
|
|
625
|
+
const flowsFile = flowsYamlPath(customer.idn);
|
|
626
|
+
let flowsBackup = '';
|
|
627
|
+
if (await fs.pathExists(flowsFile)) {
|
|
628
|
+
flowsBackup = await fs.readFile(flowsFile, 'utf8');
|
|
629
|
+
const backupPath = `${flowsFile}.backup`;
|
|
630
|
+
await fs.writeFile(backupPath, flowsBackup, 'utf8');
|
|
631
|
+
if (verbose)
|
|
632
|
+
console.log(`✓ Created flows.yaml backup at ${backupPath}`);
|
|
633
|
+
}
|
|
634
|
+
// Re-fetch agents for flows.yaml regeneration
|
|
635
|
+
const agentsForFlows = [];
|
|
636
|
+
for (const projectData of Object.values(projects)) {
|
|
637
|
+
const projectAgents = await listAgents(client, projectData.projectId);
|
|
638
|
+
agentsForFlows.push(...projectAgents);
|
|
639
|
+
}
|
|
640
|
+
// Regenerate flows.yaml
|
|
641
|
+
await generateFlowsYaml(client, customer, agentsForFlows, verbose);
|
|
642
|
+
// Update flows.yaml hash
|
|
643
|
+
if (await fs.pathExists(flowsFile)) {
|
|
644
|
+
const newFlowsContent = await fs.readFile(flowsFile, 'utf8');
|
|
645
|
+
newHashes[flowsFile] = sha256(newFlowsContent);
|
|
646
|
+
// Compare format with backup
|
|
647
|
+
if (flowsBackup) {
|
|
648
|
+
const sizeDiff = newFlowsContent.length - flowsBackup.length;
|
|
649
|
+
if (verbose) {
|
|
650
|
+
console.log(`✓ Regenerated flows.yaml (size change: ${sizeDiff > 0 ? '+' : ''}${sizeDiff} chars)`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
console.log('↑ Regenerated flows.yaml due to metadata changes');
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
console.error(`❌ Failed to regenerate flows.yaml: ${error instanceof Error ? error.message : String(error)}`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
447
660
|
await saveHashes(newHashes, customer.idn);
|
|
448
661
|
console.log(pushed ? `✅ Push complete. ${pushed} file(s) updated.` : '✅ Nothing to push.');
|
|
449
662
|
}
|
|
@@ -514,6 +727,48 @@ export async function status(customer, verbose = false) {
|
|
|
514
727
|
console.log(` ✓ Unchanged: ${currentPath}`);
|
|
515
728
|
}
|
|
516
729
|
}
|
|
730
|
+
// Check metadata.yaml files for changes (after skill files)
|
|
731
|
+
for (const [skillIdn] of Object.entries(flowObj.skills)) {
|
|
732
|
+
const metadataPath = projectIdn ?
|
|
733
|
+
skillMetadataPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn) :
|
|
734
|
+
skillMetadataPath(customer.idn, '', agentIdn, flowIdn, skillIdn);
|
|
735
|
+
if (await fs.pathExists(metadataPath)) {
|
|
736
|
+
const metadataContent = await fs.readFile(metadataPath, 'utf8');
|
|
737
|
+
const h = sha256(metadataContent);
|
|
738
|
+
const oldHash = hashes[metadataPath];
|
|
739
|
+
if (verbose) {
|
|
740
|
+
console.log(` 📄 ${metadataPath}`);
|
|
741
|
+
console.log(` Old hash: ${oldHash || 'none'}`);
|
|
742
|
+
console.log(` New hash: ${h}`);
|
|
743
|
+
}
|
|
744
|
+
if (oldHash !== h) {
|
|
745
|
+
console.log(`M ${metadataPath}`);
|
|
746
|
+
dirty++;
|
|
747
|
+
// Show which metadata fields changed
|
|
748
|
+
try {
|
|
749
|
+
const newMetadata = yaml.load(metadataContent);
|
|
750
|
+
console.log(` 📊 Metadata changed for skill: ${skillIdn}`);
|
|
751
|
+
if (newMetadata?.title) {
|
|
752
|
+
console.log(` • Title: ${newMetadata.title}`);
|
|
753
|
+
}
|
|
754
|
+
if (newMetadata?.runner_type) {
|
|
755
|
+
console.log(` • Runner: ${newMetadata.runner_type}`);
|
|
756
|
+
}
|
|
757
|
+
if (newMetadata?.model) {
|
|
758
|
+
console.log(` • Model: ${newMetadata.model.provider_idn}/${newMetadata.model.model_idn}`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
catch (e) {
|
|
762
|
+
// Fallback to simple message
|
|
763
|
+
if (verbose)
|
|
764
|
+
console.log(` 🔄 Modified: metadata.yaml`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
else if (verbose) {
|
|
768
|
+
console.log(` ✓ Unchanged: ${metadataPath}`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
517
772
|
}
|
|
518
773
|
}
|
|
519
774
|
}
|
|
@@ -532,6 +787,54 @@ export async function status(customer, verbose = false) {
|
|
|
532
787
|
if (oldHash !== h) {
|
|
533
788
|
console.log(`M ${attributesFile}`);
|
|
534
789
|
dirty++;
|
|
790
|
+
// Show which attributes changed by comparing with backup
|
|
791
|
+
try {
|
|
792
|
+
const attributesBackupFile = customerAttributesBackupPath(customer.idn);
|
|
793
|
+
if (await fs.pathExists(attributesBackupFile)) {
|
|
794
|
+
const backupContent = await fs.readFile(attributesBackupFile, 'utf8');
|
|
795
|
+
const parseYaml = (content) => {
|
|
796
|
+
let yamlContent = content.replace(/!enum "([^"]+)"/g, '"$1"');
|
|
797
|
+
return yaml.load(yamlContent);
|
|
798
|
+
};
|
|
799
|
+
const currentData = parseYaml(content);
|
|
800
|
+
const backupData = parseYaml(backupContent);
|
|
801
|
+
if (currentData?.attributes && backupData?.attributes) {
|
|
802
|
+
const currentAttrs = new Map(currentData.attributes.map(attr => [attr.idn, attr]));
|
|
803
|
+
const backupAttrs = new Map(backupData.attributes.map(attr => [attr.idn, attr]));
|
|
804
|
+
const changedAttributes = [];
|
|
805
|
+
for (const [idn, currentAttr] of currentAttrs) {
|
|
806
|
+
const backupAttr = backupAttrs.get(idn);
|
|
807
|
+
const hasChanged = !backupAttr ||
|
|
808
|
+
currentAttr.value !== backupAttr.value ||
|
|
809
|
+
currentAttr.title !== backupAttr.title ||
|
|
810
|
+
currentAttr.description !== backupAttr.description ||
|
|
811
|
+
currentAttr.group !== backupAttr.group ||
|
|
812
|
+
currentAttr.is_hidden !== backupAttr.is_hidden;
|
|
813
|
+
if (hasChanged) {
|
|
814
|
+
changedAttributes.push(idn);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (changedAttributes.length > 0) {
|
|
818
|
+
console.log(` 📊 Changed attributes (${changedAttributes.length}):`);
|
|
819
|
+
changedAttributes.slice(0, 5).forEach(idn => {
|
|
820
|
+
const current = currentAttrs.get(idn);
|
|
821
|
+
const backup = backupAttrs.get(idn);
|
|
822
|
+
console.log(` • ${idn}: ${current?.title || 'No title'}`);
|
|
823
|
+
if (verbose) {
|
|
824
|
+
console.log(` Old: ${backup?.value || 'N/A'}`);
|
|
825
|
+
console.log(` New: ${current?.value || 'N/A'}`);
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
if (changedAttributes.length > 5) {
|
|
829
|
+
console.log(` ... and ${changedAttributes.length - 5} more`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
catch (e) {
|
|
836
|
+
// Fallback to simple message if diff analysis fails
|
|
837
|
+
}
|
|
535
838
|
if (verbose)
|
|
536
839
|
console.log(` 🔄 Modified: attributes.yaml`);
|
|
537
840
|
}
|
|
@@ -548,12 +851,29 @@ export async function status(customer, verbose = false) {
|
|
|
548
851
|
const flowsFile = flowsYamlPath(customer.idn);
|
|
549
852
|
if (await fs.pathExists(flowsFile)) {
|
|
550
853
|
try {
|
|
551
|
-
const
|
|
854
|
+
const flowsContent = await fs.readFile(flowsFile, 'utf8');
|
|
855
|
+
const h = sha256(flowsContent);
|
|
856
|
+
const oldHash = hashes[flowsFile];
|
|
552
857
|
if (verbose) {
|
|
553
858
|
console.log(`📄 flows.yaml`);
|
|
859
|
+
console.log(` Old hash: ${oldHash || 'none'}`);
|
|
860
|
+
console.log(` New hash: ${h}`);
|
|
861
|
+
}
|
|
862
|
+
if (oldHash !== h) {
|
|
863
|
+
console.log(`M ${flowsFile}`);
|
|
864
|
+
dirty++;
|
|
865
|
+
if (verbose) {
|
|
866
|
+
const flowsStats = await fs.stat(flowsFile);
|
|
867
|
+
console.log(` 🔄 Modified: flows.yaml`);
|
|
868
|
+
console.log(` 📊 Size: ${(flowsStats.size / 1024).toFixed(1)}KB`);
|
|
869
|
+
console.log(` 📅 Last modified: ${flowsStats.mtime.toISOString()}`);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
else if (verbose) {
|
|
873
|
+
const flowsStats = await fs.stat(flowsFile);
|
|
874
|
+
console.log(` ✓ Unchanged: flows.yaml`);
|
|
554
875
|
console.log(` 📅 Last modified: ${flowsStats.mtime.toISOString()}`);
|
|
555
876
|
console.log(` 📊 Size: ${(flowsStats.size / 1024).toFixed(1)}KB`);
|
|
556
|
-
console.log(` ✓ Flows file tracked`);
|
|
557
877
|
}
|
|
558
878
|
}
|
|
559
879
|
catch (error) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "newo",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "NEWO CLI: sync AI Agent skills
|
|
3
|
+
"version": "1.8.0",
|
|
4
|
+
"description": "NEWO CLI: comprehensive sync for AI Agent skills, customer attributes, and metadata between NEWO platform and local files. Multi-customer workspaces, complete change tracking, Git-first workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"newo": "dist/cli.js"
|
package/src/fsutil.ts
CHANGED
|
@@ -47,6 +47,10 @@ export function customerAttributesMapPath(customerIdn: string): string {
|
|
|
47
47
|
return path.join(customerStateDir(customerIdn), 'attributes-map.json');
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export function customerAttributesBackupPath(customerIdn: string): string {
|
|
51
|
+
return path.join(customerStateDir(customerIdn), 'attributes-backup.yaml');
|
|
52
|
+
}
|
|
53
|
+
|
|
50
54
|
// Legacy skill path - direct file
|
|
51
55
|
export function skillPath(
|
|
52
56
|
customerIdn: string,
|
package/src/sync.ts
CHANGED
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
listFlowEvents,
|
|
7
7
|
listFlowStates,
|
|
8
8
|
getProjectMeta,
|
|
9
|
-
getCustomerAttributes
|
|
9
|
+
getCustomerAttributes,
|
|
10
|
+
updateCustomerAttribute
|
|
10
11
|
} from './api.js';
|
|
11
12
|
import {
|
|
12
13
|
ensureState,
|
|
@@ -21,7 +22,8 @@ import {
|
|
|
21
22
|
skillMetadataPath,
|
|
22
23
|
flowsYamlPath,
|
|
23
24
|
customerAttributesPath,
|
|
24
|
-
customerAttributesMapPath
|
|
25
|
+
customerAttributesMapPath,
|
|
26
|
+
customerAttributesBackupPath
|
|
25
27
|
} from './fsutil.js';
|
|
26
28
|
import fs from 'fs-extra';
|
|
27
29
|
import { sha256, loadHashes, saveHashes } from './hash.js';
|
|
@@ -46,7 +48,8 @@ import type {
|
|
|
46
48
|
FlowMetadata,
|
|
47
49
|
SkillMetadata,
|
|
48
50
|
FlowEvent,
|
|
49
|
-
FlowState
|
|
51
|
+
FlowState,
|
|
52
|
+
CustomerAttribute
|
|
50
53
|
} from './types.js';
|
|
51
54
|
|
|
52
55
|
// Concurrency limits for API operations
|
|
@@ -136,13 +139,15 @@ export async function saveCustomerAttributes(
|
|
|
136
139
|
// Fix JSON string formatting to match reference (remove escape characters)
|
|
137
140
|
yamlContent = yamlContent.replace(/\\"/g, '"');
|
|
138
141
|
|
|
139
|
-
// Save
|
|
142
|
+
// Save all files: attributes.yaml, ID mapping, and backup for diff tracking
|
|
140
143
|
await writeFileSafe(customerAttributesPath(customer.idn), yamlContent);
|
|
141
144
|
await writeFileSafe(customerAttributesMapPath(customer.idn), JSON.stringify(idMapping, null, 2));
|
|
145
|
+
await writeFileSafe(customerAttributesBackupPath(customer.idn), yamlContent);
|
|
142
146
|
|
|
143
147
|
if (verbose) {
|
|
144
148
|
console.log(`✓ Saved customer attributes to ${customerAttributesPath(customer.idn)}`);
|
|
145
149
|
console.log(`✓ Saved attribute ID mapping to ${customerAttributesMapPath(customer.idn)}`);
|
|
150
|
+
console.log(`✓ Created attributes backup for diff tracking`);
|
|
146
151
|
}
|
|
147
152
|
} catch (error) {
|
|
148
153
|
console.error(`❌ Failed to save customer attributes for ${customer.idn}:`, error);
|
|
@@ -296,6 +301,13 @@ export async function pullAll(
|
|
|
296
301
|
const content = await fs.readFile(newPath, 'utf8');
|
|
297
302
|
hashes[newPath] = sha256(content);
|
|
298
303
|
|
|
304
|
+
// Track skill metadata.yaml file
|
|
305
|
+
const metadataPath = skillMetadataPath(customer.idn, projectMeta.idn, agentIdn, flowIdn, skillIdn);
|
|
306
|
+
if (await fs.pathExists(metadataPath)) {
|
|
307
|
+
const metadataContent = await fs.readFile(metadataPath, 'utf8');
|
|
308
|
+
hashes[metadataPath] = sha256(metadataContent);
|
|
309
|
+
}
|
|
310
|
+
|
|
299
311
|
// Also track legacy path for backwards compatibility during transition
|
|
300
312
|
const legacyPath = skillPath(customer.idn, projectMeta.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
301
313
|
hashes[legacyPath] = sha256(content);
|
|
@@ -314,6 +326,14 @@ export async function pullAll(
|
|
|
314
326
|
hashes[attributesFile] = sha256(attributesContent);
|
|
315
327
|
if (verbose) console.log(`✓ Added attributes.yaml to hash tracking`);
|
|
316
328
|
}
|
|
329
|
+
|
|
330
|
+
// Add flows.yaml to hash tracking
|
|
331
|
+
const flowsFile = flowsYamlPath(customer.idn);
|
|
332
|
+
if (await fs.pathExists(flowsFile)) {
|
|
333
|
+
const flowsContent = await fs.readFile(flowsFile, 'utf8');
|
|
334
|
+
hashes[flowsFile] = sha256(flowsContent);
|
|
335
|
+
if (verbose) console.log(`✓ Added flows.yaml to hash tracking`);
|
|
336
|
+
}
|
|
317
337
|
} catch (error) {
|
|
318
338
|
console.error(`❌ Failed to save customer attributes for ${customer.idn}:`, error);
|
|
319
339
|
// Don't throw - continue with the rest of the process
|
|
@@ -345,6 +365,13 @@ export async function pullAll(
|
|
|
345
365
|
const content = await fs.readFile(newPath, 'utf8');
|
|
346
366
|
allHashes[newPath] = sha256(content);
|
|
347
367
|
|
|
368
|
+
// Track skill metadata.yaml file
|
|
369
|
+
const metadataPath = skillMetadataPath(customer.idn, project.idn, agentIdn, flowIdn, skillIdn);
|
|
370
|
+
if (await fs.pathExists(metadataPath)) {
|
|
371
|
+
const metadataContent = await fs.readFile(metadataPath, 'utf8');
|
|
372
|
+
allHashes[metadataPath] = sha256(metadataContent);
|
|
373
|
+
}
|
|
374
|
+
|
|
348
375
|
// Also track legacy path for backwards compatibility during transition
|
|
349
376
|
const legacyPath = skillPath(customer.idn, project.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
350
377
|
allHashes[legacyPath] = sha256(content);
|
|
@@ -366,6 +393,14 @@ export async function pullAll(
|
|
|
366
393
|
allHashes[attributesFile] = sha256(attributesContent);
|
|
367
394
|
if (verbose) console.log(`✓ Added attributes.yaml to hash tracking`);
|
|
368
395
|
}
|
|
396
|
+
|
|
397
|
+
// Add flows.yaml to hash tracking
|
|
398
|
+
const flowsFile = flowsYamlPath(customer.idn);
|
|
399
|
+
if (await fs.pathExists(flowsFile)) {
|
|
400
|
+
const flowsContent = await fs.readFile(flowsFile, 'utf8');
|
|
401
|
+
allHashes[flowsFile] = sha256(flowsContent);
|
|
402
|
+
if (verbose) console.log(`✓ Added flows.yaml to hash tracking`);
|
|
403
|
+
}
|
|
369
404
|
} catch (error) {
|
|
370
405
|
console.error(`❌ Failed to save customer attributes for ${customer.idn}:`, error);
|
|
371
406
|
// Don't throw - continue with the rest of the process
|
|
@@ -389,6 +424,7 @@ export async function pushChanged(client: AxiosInstance, customer: CustomerConfi
|
|
|
389
424
|
if (verbose) console.log('🔄 Scanning for changes...');
|
|
390
425
|
let pushed = 0;
|
|
391
426
|
let scanned = 0;
|
|
427
|
+
let metadataChanged = false;
|
|
392
428
|
|
|
393
429
|
// Handle both old single-project format and new multi-project format with type guards
|
|
394
430
|
const projects = isProjectMap(idMapData) && idMapData.projects
|
|
@@ -495,40 +531,197 @@ export async function pushChanged(client: AxiosInstance, customer: CustomerConfi
|
|
|
495
531
|
}
|
|
496
532
|
}
|
|
497
533
|
|
|
534
|
+
// Check for metadata-only changes (when metadata changed but script didn't)
|
|
535
|
+
try {
|
|
536
|
+
for (const [projectIdn, projectData] of Object.entries(projects)) {
|
|
537
|
+
for (const [agentIdn, agentObj] of Object.entries(projectData.agents)) {
|
|
538
|
+
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
539
|
+
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
540
|
+
const metadataPath = projectIdn ?
|
|
541
|
+
skillMetadataPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn) :
|
|
542
|
+
skillMetadataPath(customer.idn, '', agentIdn, flowIdn, skillIdn);
|
|
543
|
+
|
|
544
|
+
if (await fs.pathExists(metadataPath)) {
|
|
545
|
+
const metadataContent = await fs.readFile(metadataPath, 'utf8');
|
|
546
|
+
const h = sha256(metadataContent);
|
|
547
|
+
const oldHash = oldHashes[metadataPath];
|
|
548
|
+
|
|
549
|
+
if (oldHash !== h) {
|
|
550
|
+
if (verbose) console.log(`🔄 Metadata-only change detected for ${skillIdn}, updating skill...`);
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
// Load updated metadata
|
|
554
|
+
const updatedMetadata = yaml.load(metadataContent) as SkillMetadata;
|
|
555
|
+
|
|
556
|
+
// Get current script content
|
|
557
|
+
const scriptPath = projectIdn ?
|
|
558
|
+
skillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
|
|
559
|
+
skillScriptPath(customer.idn, '', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
560
|
+
|
|
561
|
+
let scriptContent = '';
|
|
562
|
+
if (await fs.pathExists(scriptPath)) {
|
|
563
|
+
scriptContent = await fs.readFile(scriptPath, 'utf8');
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Create skill object with updated metadata
|
|
567
|
+
const skillObject = {
|
|
568
|
+
id: updatedMetadata.id,
|
|
569
|
+
title: updatedMetadata.title,
|
|
570
|
+
idn: updatedMetadata.idn,
|
|
571
|
+
prompt_script: scriptContent,
|
|
572
|
+
runner_type: updatedMetadata.runner_type,
|
|
573
|
+
model: updatedMetadata.model,
|
|
574
|
+
parameters: updatedMetadata.parameters,
|
|
575
|
+
path: updatedMetadata.path || undefined
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
await updateSkill(client, skillObject);
|
|
579
|
+
console.log(`↑ Pushed metadata update for skill: ${skillIdn} (${updatedMetadata.title})`);
|
|
580
|
+
|
|
581
|
+
newHashes[metadataPath] = h;
|
|
582
|
+
pushed++;
|
|
583
|
+
metadataChanged = true;
|
|
584
|
+
|
|
585
|
+
} catch (error) {
|
|
586
|
+
console.error(`❌ Failed to push metadata for ${skillIdn}: ${error instanceof Error ? error.message : String(error)}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
} catch (error) {
|
|
595
|
+
if (verbose) console.log(`⚠️ Metadata push check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
596
|
+
}
|
|
597
|
+
|
|
498
598
|
if (verbose) console.log(`🔄 Scanned ${scanned} files, found ${pushed} changes`);
|
|
499
599
|
|
|
500
|
-
// Check for attributes changes and push
|
|
600
|
+
// Check for attributes changes and push specific changed attributes only
|
|
501
601
|
try {
|
|
502
602
|
const attributesFile = customerAttributesPath(customer.idn);
|
|
503
603
|
const attributesMapFile = customerAttributesMapPath(customer.idn);
|
|
604
|
+
const attributesBackupFile = customerAttributesBackupPath(customer.idn);
|
|
504
605
|
|
|
505
606
|
if (await fs.pathExists(attributesFile) && await fs.pathExists(attributesMapFile)) {
|
|
506
607
|
if (verbose) console.log('🔍 Checking customer attributes for changes...');
|
|
507
608
|
|
|
508
|
-
|
|
509
|
-
const content = await fs.readFile(attributesFile, 'utf8');
|
|
510
|
-
const h = sha256(content);
|
|
511
|
-
const oldHash = oldHashes[attributesFile];
|
|
609
|
+
const currentContent = await fs.readFile(attributesFile, 'utf8');
|
|
512
610
|
|
|
513
|
-
if
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
console.log(` New hash: ${h}`);
|
|
517
|
-
}
|
|
611
|
+
// Check if backup exists for diff comparison
|
|
612
|
+
if (await fs.pathExists(attributesBackupFile)) {
|
|
613
|
+
const backupContent = await fs.readFile(attributesBackupFile, 'utf8');
|
|
518
614
|
|
|
519
|
-
|
|
520
|
-
|
|
615
|
+
if (currentContent !== backupContent) {
|
|
616
|
+
if (verbose) console.log(`🔄 Attributes file changed, analyzing differences...`);
|
|
521
617
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
newHashes[attributesFile] = h;
|
|
526
|
-
pushed++;
|
|
618
|
+
try {
|
|
619
|
+
// Load ID mapping for push operations
|
|
620
|
+
const idMapping = await fs.readJson(attributesMapFile) as Record<string, string>;
|
|
527
621
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
622
|
+
// Parse both versions to find changed attributes
|
|
623
|
+
const parseYaml = (content: string) => {
|
|
624
|
+
let yamlContent = content.replace(/!enum "([^"]+)"/g, '"$1"');
|
|
625
|
+
return yaml.load(yamlContent) as { attributes: any[] };
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const currentData = parseYaml(currentContent);
|
|
629
|
+
const backupData = parseYaml(backupContent);
|
|
630
|
+
|
|
631
|
+
if (currentData?.attributes && backupData?.attributes) {
|
|
632
|
+
// Create maps for comparison
|
|
633
|
+
const currentAttrs = new Map(currentData.attributes.map(attr => [attr.idn, attr]));
|
|
634
|
+
const backupAttrs = new Map(backupData.attributes.map(attr => [attr.idn, attr]));
|
|
635
|
+
|
|
636
|
+
let attributesPushed = 0;
|
|
637
|
+
|
|
638
|
+
// Find changed attributes
|
|
639
|
+
for (const [idn, currentAttr] of currentAttrs) {
|
|
640
|
+
const backupAttr = backupAttrs.get(idn);
|
|
641
|
+
|
|
642
|
+
// Check if attribute changed (deep comparison of key fields)
|
|
643
|
+
const hasChanged = !backupAttr ||
|
|
644
|
+
currentAttr.value !== backupAttr.value ||
|
|
645
|
+
currentAttr.title !== backupAttr.title ||
|
|
646
|
+
currentAttr.description !== backupAttr.description ||
|
|
647
|
+
currentAttr.group !== backupAttr.group ||
|
|
648
|
+
currentAttr.is_hidden !== backupAttr.is_hidden;
|
|
649
|
+
|
|
650
|
+
if (hasChanged) {
|
|
651
|
+
const attributeId = idMapping[idn];
|
|
652
|
+
if (!attributeId) {
|
|
653
|
+
if (verbose) console.log(`⚠️ Skipping ${idn} - no ID mapping`);
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Create attribute object for push
|
|
658
|
+
const attributeToUpdate: CustomerAttribute = {
|
|
659
|
+
id: attributeId,
|
|
660
|
+
idn: currentAttr.idn,
|
|
661
|
+
value: currentAttr.value,
|
|
662
|
+
title: currentAttr.title || "",
|
|
663
|
+
description: currentAttr.description || "",
|
|
664
|
+
group: currentAttr.group || "",
|
|
665
|
+
is_hidden: currentAttr.is_hidden,
|
|
666
|
+
possible_values: currentAttr.possible_values || [],
|
|
667
|
+
value_type: currentAttr.value_type?.replace(/^"?AttributeValueTypes\.(.+)"?$/, '$1') || "string"
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
await updateCustomerAttribute(client, attributeToUpdate);
|
|
671
|
+
attributesPushed++;
|
|
672
|
+
|
|
673
|
+
if (verbose) {
|
|
674
|
+
console.log(` ✓ Pushed changed attribute: ${idn}`);
|
|
675
|
+
console.log(` Old value: ${backupAttr?.value || 'N/A'}`);
|
|
676
|
+
console.log(` New value: ${currentAttr.value}`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (attributesPushed > 0) {
|
|
682
|
+
console.log(`↑ Pushed ${attributesPushed} changed customer attributes to NEWO API`);
|
|
683
|
+
|
|
684
|
+
// Show summary of what was pushed
|
|
685
|
+
console.log(` 📊 Pushed attributes:`);
|
|
686
|
+
for (const [idn, currentAttr] of currentAttrs) {
|
|
687
|
+
const backupAttr = backupAttrs.get(idn);
|
|
688
|
+
const hasChanged = !backupAttr ||
|
|
689
|
+
currentAttr.value !== backupAttr.value ||
|
|
690
|
+
currentAttr.title !== backupAttr.title ||
|
|
691
|
+
currentAttr.description !== backupAttr.description ||
|
|
692
|
+
currentAttr.group !== backupAttr.group ||
|
|
693
|
+
currentAttr.is_hidden !== backupAttr.is_hidden;
|
|
694
|
+
|
|
695
|
+
if (hasChanged) {
|
|
696
|
+
console.log(` • ${idn}: ${currentAttr.title || 'No title'}`);
|
|
697
|
+
console.log(` Value: ${currentAttr.value}`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Update backup file after successful push
|
|
702
|
+
await fs.writeFile(attributesBackupFile, currentContent, 'utf8');
|
|
703
|
+
|
|
704
|
+
newHashes[attributesFile] = sha256(currentContent);
|
|
705
|
+
pushed++;
|
|
706
|
+
} else if (verbose) {
|
|
707
|
+
console.log(` ✓ No attribute value changes detected`);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
} else {
|
|
711
|
+
console.log(`⚠️ Failed to parse attributes for comparison`);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
} catch (error) {
|
|
715
|
+
console.error(`❌ Failed to push changed attributes: ${error instanceof Error ? error.message : String(error)}`);
|
|
716
|
+
// Don't update hash/backup on failure so it will retry next time
|
|
717
|
+
}
|
|
718
|
+
} else if (verbose) {
|
|
719
|
+
console.log(` ✓ No attributes file changes`);
|
|
720
|
+
}
|
|
721
|
+
} else {
|
|
722
|
+
// No backup exists, create initial backup
|
|
723
|
+
await fs.writeFile(attributesBackupFile, currentContent, 'utf8');
|
|
724
|
+
if (verbose) console.log(`✓ Created initial attributes backup for diff tracking`);
|
|
532
725
|
}
|
|
533
726
|
} else if (verbose) {
|
|
534
727
|
console.log('ℹ️ No attributes file or ID mapping found for push checking');
|
|
@@ -537,6 +730,52 @@ export async function pushChanged(client: AxiosInstance, customer: CustomerConfi
|
|
|
537
730
|
if (verbose) console.log(`⚠️ Attributes push check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
538
731
|
}
|
|
539
732
|
|
|
733
|
+
// Regenerate flows.yaml if metadata changed
|
|
734
|
+
if (metadataChanged) {
|
|
735
|
+
try {
|
|
736
|
+
if (verbose) console.log('🔄 Metadata changed, regenerating flows.yaml...');
|
|
737
|
+
|
|
738
|
+
// Create backup of current flows.yaml for format comparison
|
|
739
|
+
const flowsFile = flowsYamlPath(customer.idn);
|
|
740
|
+
let flowsBackup = '';
|
|
741
|
+
if (await fs.pathExists(flowsFile)) {
|
|
742
|
+
flowsBackup = await fs.readFile(flowsFile, 'utf8');
|
|
743
|
+
const backupPath = `${flowsFile}.backup`;
|
|
744
|
+
await fs.writeFile(backupPath, flowsBackup, 'utf8');
|
|
745
|
+
if (verbose) console.log(`✓ Created flows.yaml backup at ${backupPath}`);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Re-fetch agents for flows.yaml regeneration
|
|
749
|
+
const agentsForFlows: Agent[] = [];
|
|
750
|
+
for (const projectData of Object.values(projects)) {
|
|
751
|
+
const projectAgents = await listAgents(client, projectData.projectId);
|
|
752
|
+
agentsForFlows.push(...projectAgents);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Regenerate flows.yaml
|
|
756
|
+
await generateFlowsYaml(client, customer, agentsForFlows, verbose);
|
|
757
|
+
|
|
758
|
+
// Update flows.yaml hash
|
|
759
|
+
if (await fs.pathExists(flowsFile)) {
|
|
760
|
+
const newFlowsContent = await fs.readFile(flowsFile, 'utf8');
|
|
761
|
+
newHashes[flowsFile] = sha256(newFlowsContent);
|
|
762
|
+
|
|
763
|
+
// Compare format with backup
|
|
764
|
+
if (flowsBackup) {
|
|
765
|
+
const sizeDiff = newFlowsContent.length - flowsBackup.length;
|
|
766
|
+
if (verbose) {
|
|
767
|
+
console.log(`✓ Regenerated flows.yaml (size change: ${sizeDiff > 0 ? '+' : ''}${sizeDiff} chars)`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
console.log('↑ Regenerated flows.yaml due to metadata changes');
|
|
773
|
+
|
|
774
|
+
} catch (error) {
|
|
775
|
+
console.error(`❌ Failed to regenerate flows.yaml: ${error instanceof Error ? error.message : String(error)}`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
540
779
|
await saveHashes(newHashes, customer.idn);
|
|
541
780
|
console.log(pushed ? `✅ Push complete. ${pushed} file(s) updated.` : '✅ Nothing to push.');
|
|
542
781
|
}
|
|
@@ -612,6 +851,51 @@ export async function status(customer: CustomerConfig, verbose: boolean = false)
|
|
|
612
851
|
console.log(` ✓ Unchanged: ${currentPath}`);
|
|
613
852
|
}
|
|
614
853
|
}
|
|
854
|
+
|
|
855
|
+
// Check metadata.yaml files for changes (after skill files)
|
|
856
|
+
for (const [skillIdn] of Object.entries(flowObj.skills)) {
|
|
857
|
+
const metadataPath = projectIdn ?
|
|
858
|
+
skillMetadataPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn) :
|
|
859
|
+
skillMetadataPath(customer.idn, '', agentIdn, flowIdn, skillIdn);
|
|
860
|
+
|
|
861
|
+
if (await fs.pathExists(metadataPath)) {
|
|
862
|
+
const metadataContent = await fs.readFile(metadataPath, 'utf8');
|
|
863
|
+
const h = sha256(metadataContent);
|
|
864
|
+
const oldHash = hashes[metadataPath];
|
|
865
|
+
|
|
866
|
+
if (verbose) {
|
|
867
|
+
console.log(` 📄 ${metadataPath}`);
|
|
868
|
+
console.log(` Old hash: ${oldHash || 'none'}`);
|
|
869
|
+
console.log(` New hash: ${h}`);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if (oldHash !== h) {
|
|
873
|
+
console.log(`M ${metadataPath}`);
|
|
874
|
+
dirty++;
|
|
875
|
+
|
|
876
|
+
// Show which metadata fields changed
|
|
877
|
+
try {
|
|
878
|
+
const newMetadata = yaml.load(metadataContent) as any;
|
|
879
|
+
|
|
880
|
+
console.log(` 📊 Metadata changed for skill: ${skillIdn}`);
|
|
881
|
+
if (newMetadata?.title) {
|
|
882
|
+
console.log(` • Title: ${newMetadata.title}`);
|
|
883
|
+
}
|
|
884
|
+
if (newMetadata?.runner_type) {
|
|
885
|
+
console.log(` • Runner: ${newMetadata.runner_type}`);
|
|
886
|
+
}
|
|
887
|
+
if (newMetadata?.model) {
|
|
888
|
+
console.log(` • Model: ${newMetadata.model.provider_idn}/${newMetadata.model.model_idn}`);
|
|
889
|
+
}
|
|
890
|
+
} catch (e) {
|
|
891
|
+
// Fallback to simple message
|
|
892
|
+
if (verbose) console.log(` 🔄 Modified: metadata.yaml`);
|
|
893
|
+
}
|
|
894
|
+
} else if (verbose) {
|
|
895
|
+
console.log(` ✓ Unchanged: ${metadataPath}`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
615
899
|
}
|
|
616
900
|
}
|
|
617
901
|
}
|
|
@@ -633,6 +917,62 @@ export async function status(customer: CustomerConfig, verbose: boolean = false)
|
|
|
633
917
|
if (oldHash !== h) {
|
|
634
918
|
console.log(`M ${attributesFile}`);
|
|
635
919
|
dirty++;
|
|
920
|
+
|
|
921
|
+
// Show which attributes changed by comparing with backup
|
|
922
|
+
try {
|
|
923
|
+
const attributesBackupFile = customerAttributesBackupPath(customer.idn);
|
|
924
|
+
if (await fs.pathExists(attributesBackupFile)) {
|
|
925
|
+
const backupContent = await fs.readFile(attributesBackupFile, 'utf8');
|
|
926
|
+
|
|
927
|
+
const parseYaml = (content: string) => {
|
|
928
|
+
let yamlContent = content.replace(/!enum "([^"]+)"/g, '"$1"');
|
|
929
|
+
return yaml.load(yamlContent) as { attributes: any[] };
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
const currentData = parseYaml(content);
|
|
933
|
+
const backupData = parseYaml(backupContent);
|
|
934
|
+
|
|
935
|
+
if (currentData?.attributes && backupData?.attributes) {
|
|
936
|
+
const currentAttrs = new Map(currentData.attributes.map(attr => [attr.idn, attr]));
|
|
937
|
+
const backupAttrs = new Map(backupData.attributes.map(attr => [attr.idn, attr]));
|
|
938
|
+
|
|
939
|
+
const changedAttributes: string[] = [];
|
|
940
|
+
|
|
941
|
+
for (const [idn, currentAttr] of currentAttrs) {
|
|
942
|
+
const backupAttr = backupAttrs.get(idn);
|
|
943
|
+
const hasChanged = !backupAttr ||
|
|
944
|
+
currentAttr.value !== backupAttr.value ||
|
|
945
|
+
currentAttr.title !== backupAttr.title ||
|
|
946
|
+
currentAttr.description !== backupAttr.description ||
|
|
947
|
+
currentAttr.group !== backupAttr.group ||
|
|
948
|
+
currentAttr.is_hidden !== backupAttr.is_hidden;
|
|
949
|
+
|
|
950
|
+
if (hasChanged) {
|
|
951
|
+
changedAttributes.push(idn);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (changedAttributes.length > 0) {
|
|
956
|
+
console.log(` 📊 Changed attributes (${changedAttributes.length}):`);
|
|
957
|
+
changedAttributes.slice(0, 5).forEach(idn => {
|
|
958
|
+
const current = currentAttrs.get(idn);
|
|
959
|
+
const backup = backupAttrs.get(idn);
|
|
960
|
+
console.log(` • ${idn}: ${current?.title || 'No title'}`);
|
|
961
|
+
if (verbose) {
|
|
962
|
+
console.log(` Old: ${backup?.value || 'N/A'}`);
|
|
963
|
+
console.log(` New: ${current?.value || 'N/A'}`);
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
if (changedAttributes.length > 5) {
|
|
967
|
+
console.log(` ... and ${changedAttributes.length - 5} more`);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
} catch (e) {
|
|
973
|
+
// Fallback to simple message if diff analysis fails
|
|
974
|
+
}
|
|
975
|
+
|
|
636
976
|
if (verbose) console.log(` 🔄 Modified: attributes.yaml`);
|
|
637
977
|
} else if (verbose) {
|
|
638
978
|
console.log(` ✓ Unchanged: attributes.yaml`);
|
|
@@ -646,12 +986,30 @@ export async function status(customer: CustomerConfig, verbose: boolean = false)
|
|
|
646
986
|
const flowsFile = flowsYamlPath(customer.idn);
|
|
647
987
|
if (await fs.pathExists(flowsFile)) {
|
|
648
988
|
try {
|
|
649
|
-
const
|
|
989
|
+
const flowsContent = await fs.readFile(flowsFile, 'utf8');
|
|
990
|
+
const h = sha256(flowsContent);
|
|
991
|
+
const oldHash = hashes[flowsFile];
|
|
992
|
+
|
|
650
993
|
if (verbose) {
|
|
651
994
|
console.log(`📄 flows.yaml`);
|
|
995
|
+
console.log(` Old hash: ${oldHash || 'none'}`);
|
|
996
|
+
console.log(` New hash: ${h}`);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (oldHash !== h) {
|
|
1000
|
+
console.log(`M ${flowsFile}`);
|
|
1001
|
+
dirty++;
|
|
1002
|
+
if (verbose) {
|
|
1003
|
+
const flowsStats = await fs.stat(flowsFile);
|
|
1004
|
+
console.log(` 🔄 Modified: flows.yaml`);
|
|
1005
|
+
console.log(` 📊 Size: ${(flowsStats.size / 1024).toFixed(1)}KB`);
|
|
1006
|
+
console.log(` 📅 Last modified: ${flowsStats.mtime.toISOString()}`);
|
|
1007
|
+
}
|
|
1008
|
+
} else if (verbose) {
|
|
1009
|
+
const flowsStats = await fs.stat(flowsFile);
|
|
1010
|
+
console.log(` ✓ Unchanged: flows.yaml`);
|
|
652
1011
|
console.log(` 📅 Last modified: ${flowsStats.mtime.toISOString()}`);
|
|
653
1012
|
console.log(` 📊 Size: ${(flowsStats.size / 1024).toFixed(1)}KB`);
|
|
654
|
-
console.log(` ✓ Flows file tracked`);
|
|
655
1013
|
}
|
|
656
1014
|
} catch (error) {
|
|
657
1015
|
if (verbose) console.log(`⚠️ Error checking flows.yaml: ${error instanceof Error ? error.message : String(error)}`);
|