newo 1.7.2 → 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 +41 -0
- package/dist/fsutil.d.ts +1 -0
- package/dist/fsutil.js +3 -0
- package/dist/sync.js +383 -27
- package/package.json +2 -2
- package/src/fsutil.ts +4 -0
- package/src/sync.ts +422 -27
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,47 @@ 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
|
+
|
|
33
|
+
## [1.7.3] - 2025-09-15
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- **Complete Attributes Change Tracking**: Full hash-based change detection for customer attributes
|
|
37
|
+
- Attributes.yaml files now included in hash tracking during pull operations
|
|
38
|
+
- Status command detects and reports modifications to attributes.yaml files
|
|
39
|
+
- Push command detects and handles attributes changes with proper synchronization
|
|
40
|
+
- Comprehensive workflow: modify → status shows change → push applies change → status shows clean
|
|
41
|
+
|
|
42
|
+
### Enhanced
|
|
43
|
+
- **File Synchronization Scope**: Extended to cover all file types in NEWO workspace
|
|
44
|
+
- Skills: .guidance and .jinja files with full hash tracking ✓
|
|
45
|
+
- Attributes: customer attributes.yaml with change detection ✓
|
|
46
|
+
- Metadata: flows.yaml and metadata.yaml files tracked ✓
|
|
47
|
+
- Multi-customer: all file types synchronized across multiple customers ✓
|
|
48
|
+
|
|
8
49
|
## [1.7.2] - 2025-09-15
|
|
9
50
|
|
|
10
51
|
### Fixed
|
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,21 +217,43 @@ 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);
|
|
221
229
|
}
|
|
222
230
|
}
|
|
223
231
|
}
|
|
224
|
-
|
|
225
|
-
// Save customer attributes
|
|
232
|
+
// Save customer attributes before hash tracking
|
|
226
233
|
try {
|
|
227
234
|
await saveCustomerAttributes(client, customer, verbose);
|
|
235
|
+
// Add attributes.yaml to hash tracking
|
|
236
|
+
const attributesFile = customerAttributesPath(customer.idn);
|
|
237
|
+
if (await fs.pathExists(attributesFile)) {
|
|
238
|
+
const attributesContent = await fs.readFile(attributesFile, 'utf8');
|
|
239
|
+
hashes[attributesFile] = sha256(attributesContent);
|
|
240
|
+
if (verbose)
|
|
241
|
+
console.log(`✓ Added attributes.yaml to hash tracking`);
|
|
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
|
+
}
|
|
228
251
|
}
|
|
229
252
|
catch (error) {
|
|
230
253
|
console.error(`❌ Failed to save customer attributes for ${customer.idn}:`, error);
|
|
231
254
|
// Don't throw - continue with the rest of the process
|
|
232
255
|
}
|
|
256
|
+
await saveHashes(hashes, customer.idn);
|
|
233
257
|
return;
|
|
234
258
|
}
|
|
235
259
|
// Multi-project mode
|
|
@@ -253,6 +277,12 @@ export async function pullAll(client, customer, projectId = null, verbose = fals
|
|
|
253
277
|
const newPath = skillScriptPath(customer.idn, project.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
254
278
|
const content = await fs.readFile(newPath, 'utf8');
|
|
255
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
|
+
}
|
|
256
286
|
// Also track legacy path for backwards compatibility during transition
|
|
257
287
|
const legacyPath = skillPath(customer.idn, project.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
258
288
|
allHashes[legacyPath] = sha256(content);
|
|
@@ -261,15 +291,31 @@ export async function pullAll(client, customer, projectId = null, verbose = fals
|
|
|
261
291
|
}
|
|
262
292
|
}
|
|
263
293
|
await fs.writeJson(mapPath(customer.idn), idMap, { spaces: 2 });
|
|
264
|
-
|
|
265
|
-
// Save customer attributes
|
|
294
|
+
// Save customer attributes before hash tracking
|
|
266
295
|
try {
|
|
267
296
|
await saveCustomerAttributes(client, customer, verbose);
|
|
297
|
+
// Add attributes.yaml to hash tracking
|
|
298
|
+
const attributesFile = customerAttributesPath(customer.idn);
|
|
299
|
+
if (await fs.pathExists(attributesFile)) {
|
|
300
|
+
const attributesContent = await fs.readFile(attributesFile, 'utf8');
|
|
301
|
+
allHashes[attributesFile] = sha256(attributesContent);
|
|
302
|
+
if (verbose)
|
|
303
|
+
console.log(`✓ Added attributes.yaml to hash tracking`);
|
|
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
|
+
}
|
|
268
313
|
}
|
|
269
314
|
catch (error) {
|
|
270
315
|
console.error(`❌ Failed to save customer attributes for ${customer.idn}:`, error);
|
|
271
316
|
// Don't throw - continue with the rest of the process
|
|
272
317
|
}
|
|
318
|
+
await saveHashes(allHashes, customer.idn);
|
|
273
319
|
}
|
|
274
320
|
export async function pushChanged(client, customer, verbose = false) {
|
|
275
321
|
await ensureState(customer.idn);
|
|
@@ -287,6 +333,7 @@ export async function pushChanged(client, customer, verbose = false) {
|
|
|
287
333
|
console.log('🔄 Scanning for changes...');
|
|
288
334
|
let pushed = 0;
|
|
289
335
|
let scanned = 0;
|
|
336
|
+
let metadataChanged = false;
|
|
290
337
|
// Handle both old single-project format and new multi-project format with type guards
|
|
291
338
|
const projects = isProjectMap(idMapData) && idMapData.projects
|
|
292
339
|
? idMapData.projects
|
|
@@ -387,24 +434,178 @@ export async function pushChanged(client, customer, verbose = false) {
|
|
|
387
434
|
}
|
|
388
435
|
}
|
|
389
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
|
+
}
|
|
390
495
|
if (verbose)
|
|
391
496
|
console.log(`🔄 Scanned ${scanned} files, found ${pushed} changes`);
|
|
392
|
-
// Check for attributes changes and push
|
|
497
|
+
// Check for attributes changes and push specific changed attributes only
|
|
393
498
|
try {
|
|
394
499
|
const attributesFile = customerAttributesPath(customer.idn);
|
|
395
500
|
const attributesMapFile = customerAttributesMapPath(customer.idn);
|
|
501
|
+
const attributesBackupFile = customerAttributesBackupPath(customer.idn);
|
|
396
502
|
if (await fs.pathExists(attributesFile) && await fs.pathExists(attributesMapFile)) {
|
|
397
503
|
if (verbose)
|
|
398
504
|
console.log('🔍 Checking customer attributes for changes...');
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
+
}
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
// No backup exists, create initial backup
|
|
606
|
+
await fs.writeFile(attributesBackupFile, currentContent, 'utf8');
|
|
607
|
+
if (verbose)
|
|
608
|
+
console.log(`✓ Created initial attributes backup for diff tracking`);
|
|
408
609
|
}
|
|
409
610
|
}
|
|
410
611
|
else if (verbose) {
|
|
@@ -415,6 +616,47 @@ export async function pushChanged(client, customer, verbose = false) {
|
|
|
415
616
|
if (verbose)
|
|
416
617
|
console.log(`⚠️ Attributes push check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
417
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
|
+
}
|
|
418
660
|
await saveHashes(newHashes, customer.idn);
|
|
419
661
|
console.log(pushed ? `✅ Push complete. ${pushed} file(s) updated.` : '✅ Nothing to push.');
|
|
420
662
|
}
|
|
@@ -485,6 +727,48 @@ export async function status(customer, verbose = false) {
|
|
|
485
727
|
console.log(` ✓ Unchanged: ${currentPath}`);
|
|
486
728
|
}
|
|
487
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
|
+
}
|
|
488
772
|
}
|
|
489
773
|
}
|
|
490
774
|
}
|
|
@@ -492,16 +776,71 @@ export async function status(customer, verbose = false) {
|
|
|
492
776
|
try {
|
|
493
777
|
const attributesFile = customerAttributesPath(customer.idn);
|
|
494
778
|
if (await fs.pathExists(attributesFile)) {
|
|
495
|
-
const
|
|
496
|
-
const
|
|
779
|
+
const content = await fs.readFile(attributesFile, 'utf8');
|
|
780
|
+
const h = sha256(content);
|
|
781
|
+
const oldHash = hashes[attributesFile];
|
|
497
782
|
if (verbose) {
|
|
498
|
-
console.log(`📄 ${
|
|
499
|
-
console.log(`
|
|
500
|
-
console.log(`
|
|
783
|
+
console.log(`📄 ${attributesFile}`);
|
|
784
|
+
console.log(` Old hash: ${oldHash || 'none'}`);
|
|
785
|
+
console.log(` New hash: ${h}`);
|
|
786
|
+
}
|
|
787
|
+
if (oldHash !== h) {
|
|
788
|
+
console.log(`M ${attributesFile}`);
|
|
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
|
+
}
|
|
838
|
+
if (verbose)
|
|
839
|
+
console.log(` 🔄 Modified: attributes.yaml`);
|
|
840
|
+
}
|
|
841
|
+
else if (verbose) {
|
|
842
|
+
console.log(` ✓ Unchanged: attributes.yaml`);
|
|
501
843
|
}
|
|
502
|
-
// For now, just report the file exists (change detection would require timestamp tracking)
|
|
503
|
-
if (verbose)
|
|
504
|
-
console.log(` ✓ Attributes file tracked`);
|
|
505
844
|
}
|
|
506
845
|
}
|
|
507
846
|
catch (error) {
|
|
@@ -512,12 +851,29 @@ export async function status(customer, verbose = false) {
|
|
|
512
851
|
const flowsFile = flowsYamlPath(customer.idn);
|
|
513
852
|
if (await fs.pathExists(flowsFile)) {
|
|
514
853
|
try {
|
|
515
|
-
const
|
|
854
|
+
const flowsContent = await fs.readFile(flowsFile, 'utf8');
|
|
855
|
+
const h = sha256(flowsContent);
|
|
856
|
+
const oldHash = hashes[flowsFile];
|
|
516
857
|
if (verbose) {
|
|
517
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`);
|
|
518
875
|
console.log(` 📅 Last modified: ${flowsStats.mtime.toISOString()}`);
|
|
519
876
|
console.log(` 📊 Size: ${(flowsStats.size / 1024).toFixed(1)}KB`);
|
|
520
|
-
console.log(` ✓ Flows file tracked`);
|
|
521
877
|
}
|
|
522
878
|
}
|
|
523
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,21 +301,45 @@ 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);
|
|
302
314
|
}
|
|
303
315
|
}
|
|
304
316
|
}
|
|
305
|
-
await saveHashes(hashes, customer.idn);
|
|
306
317
|
|
|
307
|
-
// Save customer attributes
|
|
318
|
+
// Save customer attributes before hash tracking
|
|
308
319
|
try {
|
|
309
320
|
await saveCustomerAttributes(client, customer, verbose);
|
|
321
|
+
|
|
322
|
+
// Add attributes.yaml to hash tracking
|
|
323
|
+
const attributesFile = customerAttributesPath(customer.idn);
|
|
324
|
+
if (await fs.pathExists(attributesFile)) {
|
|
325
|
+
const attributesContent = await fs.readFile(attributesFile, 'utf8');
|
|
326
|
+
hashes[attributesFile] = sha256(attributesContent);
|
|
327
|
+
if (verbose) console.log(`✓ Added attributes.yaml to hash tracking`);
|
|
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
|
+
}
|
|
310
337
|
} catch (error) {
|
|
311
338
|
console.error(`❌ Failed to save customer attributes for ${customer.idn}:`, error);
|
|
312
339
|
// Don't throw - continue with the rest of the process
|
|
313
340
|
}
|
|
341
|
+
|
|
342
|
+
await saveHashes(hashes, customer.idn);
|
|
314
343
|
return;
|
|
315
344
|
}
|
|
316
345
|
|
|
@@ -336,6 +365,13 @@ export async function pullAll(
|
|
|
336
365
|
const content = await fs.readFile(newPath, 'utf8');
|
|
337
366
|
allHashes[newPath] = sha256(content);
|
|
338
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
|
+
|
|
339
375
|
// Also track legacy path for backwards compatibility during transition
|
|
340
376
|
const legacyPath = skillPath(customer.idn, project.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
341
377
|
allHashes[legacyPath] = sha256(content);
|
|
@@ -345,15 +381,32 @@ export async function pullAll(
|
|
|
345
381
|
}
|
|
346
382
|
|
|
347
383
|
await fs.writeJson(mapPath(customer.idn), idMap, { spaces: 2 });
|
|
348
|
-
await saveHashes(allHashes, customer.idn);
|
|
349
384
|
|
|
350
|
-
// Save customer attributes
|
|
385
|
+
// Save customer attributes before hash tracking
|
|
351
386
|
try {
|
|
352
387
|
await saveCustomerAttributes(client, customer, verbose);
|
|
388
|
+
|
|
389
|
+
// Add attributes.yaml to hash tracking
|
|
390
|
+
const attributesFile = customerAttributesPath(customer.idn);
|
|
391
|
+
if (await fs.pathExists(attributesFile)) {
|
|
392
|
+
const attributesContent = await fs.readFile(attributesFile, 'utf8');
|
|
393
|
+
allHashes[attributesFile] = sha256(attributesContent);
|
|
394
|
+
if (verbose) console.log(`✓ Added attributes.yaml to hash tracking`);
|
|
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
|
+
}
|
|
353
404
|
} catch (error) {
|
|
354
405
|
console.error(`❌ Failed to save customer attributes for ${customer.idn}:`, error);
|
|
355
406
|
// Don't throw - continue with the rest of the process
|
|
356
407
|
}
|
|
408
|
+
|
|
409
|
+
await saveHashes(allHashes, customer.idn);
|
|
357
410
|
}
|
|
358
411
|
|
|
359
412
|
export async function pushChanged(client: AxiosInstance, customer: CustomerConfig, verbose: boolean = false): Promise<void> {
|
|
@@ -371,6 +424,7 @@ export async function pushChanged(client: AxiosInstance, customer: CustomerConfi
|
|
|
371
424
|
if (verbose) console.log('🔄 Scanning for changes...');
|
|
372
425
|
let pushed = 0;
|
|
373
426
|
let scanned = 0;
|
|
427
|
+
let metadataChanged = false;
|
|
374
428
|
|
|
375
429
|
// Handle both old single-project format and new multi-project format with type guards
|
|
376
430
|
const projects = isProjectMap(idMapData) && idMapData.projects
|
|
@@ -477,27 +531,197 @@ export async function pushChanged(client: AxiosInstance, customer: CustomerConfi
|
|
|
477
531
|
}
|
|
478
532
|
}
|
|
479
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
|
+
|
|
480
598
|
if (verbose) console.log(`🔄 Scanned ${scanned} files, found ${pushed} changes`);
|
|
481
599
|
|
|
482
|
-
// Check for attributes changes and push
|
|
600
|
+
// Check for attributes changes and push specific changed attributes only
|
|
483
601
|
try {
|
|
484
602
|
const attributesFile = customerAttributesPath(customer.idn);
|
|
485
603
|
const attributesMapFile = customerAttributesMapPath(customer.idn);
|
|
604
|
+
const attributesBackupFile = customerAttributesBackupPath(customer.idn);
|
|
486
605
|
|
|
487
606
|
if (await fs.pathExists(attributesFile) && await fs.pathExists(attributesMapFile)) {
|
|
488
607
|
if (verbose) console.log('🔍 Checking customer attributes for changes...');
|
|
489
608
|
|
|
490
|
-
|
|
491
|
-
const attributesStats = await fs.stat(attributesFile);
|
|
492
|
-
const idMapping = await fs.readJson(attributesMapFile) as Record<string, string>;
|
|
609
|
+
const currentContent = await fs.readFile(attributesFile, 'utf8');
|
|
493
610
|
|
|
494
|
-
//
|
|
495
|
-
|
|
611
|
+
// Check if backup exists for diff comparison
|
|
612
|
+
if (await fs.pathExists(attributesBackupFile)) {
|
|
613
|
+
const backupContent = await fs.readFile(attributesBackupFile, 'utf8');
|
|
496
614
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
615
|
+
if (currentContent !== backupContent) {
|
|
616
|
+
if (verbose) console.log(`🔄 Attributes file changed, analyzing differences...`);
|
|
617
|
+
|
|
618
|
+
try {
|
|
619
|
+
// Load ID mapping for push operations
|
|
620
|
+
const idMapping = await fs.readJson(attributesMapFile) as Record<string, string>;
|
|
621
|
+
|
|
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`);
|
|
501
725
|
}
|
|
502
726
|
} else if (verbose) {
|
|
503
727
|
console.log('ℹ️ No attributes file or ID mapping found for push checking');
|
|
@@ -506,6 +730,52 @@ export async function pushChanged(client: AxiosInstance, customer: CustomerConfi
|
|
|
506
730
|
if (verbose) console.log(`⚠️ Attributes push check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
507
731
|
}
|
|
508
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
|
+
|
|
509
779
|
await saveHashes(newHashes, customer.idn);
|
|
510
780
|
console.log(pushed ? `✅ Push complete. ${pushed} file(s) updated.` : '✅ Nothing to push.');
|
|
511
781
|
}
|
|
@@ -581,6 +851,51 @@ export async function status(customer: CustomerConfig, verbose: boolean = false)
|
|
|
581
851
|
console.log(` ✓ Unchanged: ${currentPath}`);
|
|
582
852
|
}
|
|
583
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
|
+
}
|
|
584
899
|
}
|
|
585
900
|
}
|
|
586
901
|
}
|
|
@@ -589,17 +904,79 @@ export async function status(customer: CustomerConfig, verbose: boolean = false)
|
|
|
589
904
|
try {
|
|
590
905
|
const attributesFile = customerAttributesPath(customer.idn);
|
|
591
906
|
if (await fs.pathExists(attributesFile)) {
|
|
592
|
-
const
|
|
593
|
-
const
|
|
907
|
+
const content = await fs.readFile(attributesFile, 'utf8');
|
|
908
|
+
const h = sha256(content);
|
|
909
|
+
const oldHash = hashes[attributesFile];
|
|
594
910
|
|
|
595
911
|
if (verbose) {
|
|
596
|
-
console.log(`📄 ${
|
|
597
|
-
console.log(`
|
|
598
|
-
console.log(`
|
|
912
|
+
console.log(`📄 ${attributesFile}`);
|
|
913
|
+
console.log(` Old hash: ${oldHash || 'none'}`);
|
|
914
|
+
console.log(` New hash: ${h}`);
|
|
599
915
|
}
|
|
600
916
|
|
|
601
|
-
|
|
602
|
-
|
|
917
|
+
if (oldHash !== h) {
|
|
918
|
+
console.log(`M ${attributesFile}`);
|
|
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
|
+
|
|
976
|
+
if (verbose) console.log(` 🔄 Modified: attributes.yaml`);
|
|
977
|
+
} else if (verbose) {
|
|
978
|
+
console.log(` ✓ Unchanged: attributes.yaml`);
|
|
979
|
+
}
|
|
603
980
|
}
|
|
604
981
|
} catch (error) {
|
|
605
982
|
if (verbose) console.log(`⚠️ Error checking attributes: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -609,12 +986,30 @@ export async function status(customer: CustomerConfig, verbose: boolean = false)
|
|
|
609
986
|
const flowsFile = flowsYamlPath(customer.idn);
|
|
610
987
|
if (await fs.pathExists(flowsFile)) {
|
|
611
988
|
try {
|
|
612
|
-
const
|
|
989
|
+
const flowsContent = await fs.readFile(flowsFile, 'utf8');
|
|
990
|
+
const h = sha256(flowsContent);
|
|
991
|
+
const oldHash = hashes[flowsFile];
|
|
992
|
+
|
|
613
993
|
if (verbose) {
|
|
614
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`);
|
|
615
1011
|
console.log(` 📅 Last modified: ${flowsStats.mtime.toISOString()}`);
|
|
616
1012
|
console.log(` 📊 Size: ${(flowsStats.size / 1024).toFixed(1)}KB`);
|
|
617
|
-
console.log(` ✓ Flows file tracked`);
|
|
618
1013
|
}
|
|
619
1014
|
} catch (error) {
|
|
620
1015
|
if (verbose) console.log(`⚠️ Error checking flows.yaml: ${error instanceof Error ? error.message : String(error)}`);
|