newo 1.9.1 → 2.0.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 +131 -0
- package/README.md +68 -20
- package/dist/cli/commands/conversations.d.ts +3 -0
- package/dist/cli/commands/conversations.js +38 -0
- package/dist/cli/commands/help.d.ts +5 -0
- package/dist/cli/commands/help.js +50 -0
- package/dist/cli/commands/import-akb.d.ts +3 -0
- package/dist/cli/commands/import-akb.js +62 -0
- package/dist/cli/commands/list-customers.d.ts +3 -0
- package/dist/cli/commands/list-customers.js +13 -0
- package/dist/cli/commands/meta.d.ts +3 -0
- package/dist/cli/commands/meta.js +19 -0
- package/dist/cli/commands/pull-attributes.d.ts +3 -0
- package/dist/cli/commands/pull-attributes.js +16 -0
- package/dist/cli/commands/pull.d.ts +3 -0
- package/dist/cli/commands/pull.js +34 -0
- package/dist/cli/commands/push.d.ts +3 -0
- package/dist/cli/commands/push.js +39 -0
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.js +22 -0
- package/dist/cli/customer-selection.d.ts +23 -0
- package/dist/cli/customer-selection.js +110 -0
- package/dist/cli/errors.d.ts +9 -0
- package/dist/cli/errors.js +111 -0
- package/dist/cli.js +66 -463
- package/dist/fsutil.js +1 -1
- package/dist/sync/attributes.d.ts +7 -0
- package/dist/sync/attributes.js +90 -0
- package/dist/sync/conversations.d.ts +7 -0
- package/dist/sync/conversations.js +218 -0
- package/dist/sync/metadata.d.ts +8 -0
- package/dist/sync/metadata.js +124 -0
- package/dist/sync/projects.d.ts +13 -0
- package/dist/sync/projects.js +283 -0
- package/dist/sync/push.d.ts +7 -0
- package/dist/sync/push.js +171 -0
- package/dist/sync/skill-files.d.ts +42 -0
- package/dist/sync/skill-files.js +121 -0
- package/dist/sync/status.d.ts +6 -0
- package/dist/sync/status.js +247 -0
- package/dist/sync.d.ts +10 -8
- package/dist/sync.js +12 -1197
- package/dist/types.d.ts +0 -1
- package/package.json +2 -2
- package/src/cli/commands/conversations.ts +47 -0
- package/src/cli/commands/help.ts +50 -0
- package/src/cli/commands/import-akb.ts +71 -0
- package/src/cli/commands/list-customers.ts +14 -0
- package/src/cli/commands/meta.ts +26 -0
- package/src/cli/commands/pull-attributes.ts +23 -0
- package/src/cli/commands/pull.ts +43 -0
- package/src/cli/commands/push.ts +47 -0
- package/src/cli/commands/status.ts +30 -0
- package/src/cli/customer-selection.ts +135 -0
- package/src/cli/errors.ts +111 -0
- package/src/cli.ts +77 -471
- package/src/fsutil.ts +1 -1
- package/src/sync/attributes.ts +110 -0
- package/src/sync/conversations.ts +257 -0
- package/src/sync/metadata.ts +153 -0
- package/src/sync/projects.ts +359 -0
- package/src/sync/push.ts +200 -0
- package/src/sync/skill-files.ts +176 -0
- package/src/sync/status.ts +277 -0
- package/src/sync.ts +14 -1389
- package/src/types.ts +0 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill file management utilities
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { sha256 } from '../hash.js';
|
|
7
|
+
import { skillFolderPath } from '../fsutil.js';
|
|
8
|
+
import type { RunnerType } from '../types.js';
|
|
9
|
+
|
|
10
|
+
export interface SkillFile {
|
|
11
|
+
filePath: string;
|
|
12
|
+
fileName: string;
|
|
13
|
+
extension: string;
|
|
14
|
+
content: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SkillFileValidation {
|
|
18
|
+
isValid: boolean;
|
|
19
|
+
files: SkillFile[];
|
|
20
|
+
warnings: string[];
|
|
21
|
+
errors: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the correct file extension for a runner type
|
|
26
|
+
*/
|
|
27
|
+
export function getExtensionForRunner(runnerType: RunnerType): string {
|
|
28
|
+
switch (runnerType) {
|
|
29
|
+
case 'guidance':
|
|
30
|
+
return 'guidance';
|
|
31
|
+
case 'nsl':
|
|
32
|
+
return 'jinja';
|
|
33
|
+
default:
|
|
34
|
+
return 'guidance';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generate IDN-based script file path
|
|
40
|
+
*/
|
|
41
|
+
export function getIdnBasedScriptPath(
|
|
42
|
+
customerIdn: string,
|
|
43
|
+
projectIdn: string,
|
|
44
|
+
agentIdn: string,
|
|
45
|
+
flowIdn: string,
|
|
46
|
+
skillIdn: string,
|
|
47
|
+
runnerType: RunnerType
|
|
48
|
+
): string {
|
|
49
|
+
const extension = getExtensionForRunner(runnerType);
|
|
50
|
+
const folderPath = skillFolderPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn);
|
|
51
|
+
return path.join(folderPath, `${skillIdn}.${extension}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Find all script files in a skill folder
|
|
56
|
+
*/
|
|
57
|
+
export async function findSkillScriptFiles(skillFolderPath: string): Promise<SkillFile[]> {
|
|
58
|
+
if (!(await fs.pathExists(skillFolderPath))) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const files = await fs.readdir(skillFolderPath);
|
|
63
|
+
const scriptFiles: SkillFile[] = [];
|
|
64
|
+
|
|
65
|
+
for (const fileName of files) {
|
|
66
|
+
const filePath = path.join(skillFolderPath, fileName);
|
|
67
|
+
const stats = await fs.stat(filePath);
|
|
68
|
+
|
|
69
|
+
if (stats.isFile()) {
|
|
70
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
71
|
+
|
|
72
|
+
// Check for script file extensions
|
|
73
|
+
if (['.jinja', '.guidance', '.nsl'].includes(ext)) {
|
|
74
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
75
|
+
scriptFiles.push({
|
|
76
|
+
filePath,
|
|
77
|
+
fileName,
|
|
78
|
+
extension: ext.slice(1), // Remove the dot
|
|
79
|
+
content
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return scriptFiles;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Validate skill folder has exactly one script file
|
|
90
|
+
*/
|
|
91
|
+
export async function validateSkillFolder(
|
|
92
|
+
customerIdn: string,
|
|
93
|
+
projectIdn: string,
|
|
94
|
+
agentIdn: string,
|
|
95
|
+
flowIdn: string,
|
|
96
|
+
skillIdn: string
|
|
97
|
+
): Promise<SkillFileValidation> {
|
|
98
|
+
const folderPath = skillFolderPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn);
|
|
99
|
+
const files = await findSkillScriptFiles(folderPath);
|
|
100
|
+
|
|
101
|
+
const warnings: string[] = [];
|
|
102
|
+
const errors: string[] = [];
|
|
103
|
+
|
|
104
|
+
if (files.length === 0) {
|
|
105
|
+
errors.push(`No script files found in skill folder: ${skillIdn}`);
|
|
106
|
+
} else if (files.length > 1) {
|
|
107
|
+
errors.push(`Multiple script files found in skill ${skillIdn}: ${files.map(f => f.fileName).join(', ')}`);
|
|
108
|
+
warnings.push(`Only one script file allowed per skill. Remove extra files and keep one.`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
isValid: files.length === 1,
|
|
113
|
+
files,
|
|
114
|
+
warnings,
|
|
115
|
+
errors
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get the single skill script file (if valid)
|
|
121
|
+
*/
|
|
122
|
+
export async function getSingleSkillFile(
|
|
123
|
+
customerIdn: string,
|
|
124
|
+
projectIdn: string,
|
|
125
|
+
agentIdn: string,
|
|
126
|
+
flowIdn: string,
|
|
127
|
+
skillIdn: string
|
|
128
|
+
): Promise<SkillFile | null> {
|
|
129
|
+
const validation = await validateSkillFolder(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn);
|
|
130
|
+
|
|
131
|
+
if (validation.isValid && validation.files.length === 1) {
|
|
132
|
+
return validation.files[0]!;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if skill script content is different from target content
|
|
140
|
+
*/
|
|
141
|
+
export function isContentDifferent(existingContent: string, newContent: string): boolean {
|
|
142
|
+
return sha256(existingContent.trim()) !== sha256(newContent.trim());
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Interactive overwrite confirmation
|
|
147
|
+
*/
|
|
148
|
+
export async function askForOverwrite(skillIdn: string, existingFile: string, newFile: string): Promise<boolean> {
|
|
149
|
+
const readline = await import('readline');
|
|
150
|
+
const rl = readline.createInterface({
|
|
151
|
+
input: process.stdin,
|
|
152
|
+
output: process.stdout
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
console.log(`\n⚠️ File exists for skill ${skillIdn}:`);
|
|
156
|
+
console.log(` Existing: ${existingFile}`);
|
|
157
|
+
console.log(` New: ${newFile}`);
|
|
158
|
+
|
|
159
|
+
const answer = await new Promise<string>((resolve) => {
|
|
160
|
+
rl.question('Overwrite? (y)es/(n)o/(a)ll/(q)uit: ', resolve);
|
|
161
|
+
});
|
|
162
|
+
rl.close();
|
|
163
|
+
|
|
164
|
+
const choice = answer.toLowerCase().trim();
|
|
165
|
+
|
|
166
|
+
if (choice === 'q' || choice === 'quit') {
|
|
167
|
+
console.log('❌ Pull operation cancelled by user');
|
|
168
|
+
process.exit(0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (choice === 'a' || choice === 'all') {
|
|
172
|
+
return true; // This should be handled by caller to set global overwrite mode
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return choice === 'y' || choice === 'yes';
|
|
176
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status checking module
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import yaml from 'js-yaml';
|
|
6
|
+
import { sha256, loadHashes } from '../hash.js';
|
|
7
|
+
import {
|
|
8
|
+
ensureState,
|
|
9
|
+
mapPath,
|
|
10
|
+
skillMetadataPath,
|
|
11
|
+
customerAttributesPath,
|
|
12
|
+
customerAttributesBackupPath,
|
|
13
|
+
flowsYamlPath
|
|
14
|
+
} from '../fsutil.js';
|
|
15
|
+
import {
|
|
16
|
+
validateSkillFolder,
|
|
17
|
+
getSingleSkillFile
|
|
18
|
+
} from './skill-files.js';
|
|
19
|
+
import type {
|
|
20
|
+
CustomerConfig,
|
|
21
|
+
ProjectMap,
|
|
22
|
+
LegacyProjectMap,
|
|
23
|
+
ProjectData
|
|
24
|
+
} from '../types.js';
|
|
25
|
+
|
|
26
|
+
// Type guards for project map formats
|
|
27
|
+
function isProjectMap(x: unknown): x is ProjectMap {
|
|
28
|
+
return typeof x === 'object' && x !== null && 'projects' in x;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isLegacyProjectMap(x: unknown): x is LegacyProjectMap {
|
|
32
|
+
return typeof x === 'object' && x !== null && 'projectId' in x && 'agents' in x;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check status of files for a customer
|
|
37
|
+
*/
|
|
38
|
+
export async function status(customer: CustomerConfig, verbose: boolean = false): Promise<void> {
|
|
39
|
+
await ensureState(customer.idn);
|
|
40
|
+
if (!(await fs.pathExists(mapPath(customer.idn)))) {
|
|
41
|
+
console.log(`No map for customer ${customer.idn}. Run \`newo pull --customer ${customer.idn}\` first.`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (verbose) console.log(`📋 Loading project mapping and hashes for customer ${customer.idn}...`);
|
|
46
|
+
const idMapData = await fs.readJson(mapPath(customer.idn)) as unknown;
|
|
47
|
+
const hashes = await loadHashes(customer.idn);
|
|
48
|
+
let dirty = 0;
|
|
49
|
+
|
|
50
|
+
// Handle both old single-project format and new multi-project format with type guards
|
|
51
|
+
const projects = isProjectMap(idMapData) && idMapData.projects
|
|
52
|
+
? idMapData.projects
|
|
53
|
+
: isLegacyProjectMap(idMapData)
|
|
54
|
+
? { '': idMapData as ProjectData }
|
|
55
|
+
: (() => { throw new Error('Invalid project map format'); })();
|
|
56
|
+
|
|
57
|
+
for (const [projectIdn, projectData] of Object.entries(projects)) {
|
|
58
|
+
if (verbose && projectIdn) console.log(`📁 Checking project: ${projectIdn}`);
|
|
59
|
+
|
|
60
|
+
for (const [agentIdn, agentObj] of Object.entries(projectData.agents)) {
|
|
61
|
+
if (verbose) console.log(` 📁 Checking agent: ${agentIdn}`);
|
|
62
|
+
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
63
|
+
if (verbose) console.log(` 📁 Checking flow: ${flowIdn}`);
|
|
64
|
+
for (const [skillIdn] of Object.entries(flowObj.skills)) {
|
|
65
|
+
// Validate skill folder and show warnings
|
|
66
|
+
const validation = await validateSkillFolder(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn);
|
|
67
|
+
|
|
68
|
+
if (!validation.isValid) {
|
|
69
|
+
// Show warnings and errors
|
|
70
|
+
validation.errors.forEach(error => {
|
|
71
|
+
console.error(`❌ ${error}`);
|
|
72
|
+
});
|
|
73
|
+
validation.warnings.forEach(warning => {
|
|
74
|
+
console.warn(`⚠️ ${warning}`);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (validation.files.length > 1) {
|
|
78
|
+
console.warn(`⚠️ Multiple script files in skill ${skillIdn}:`);
|
|
79
|
+
validation.files.forEach(file => {
|
|
80
|
+
console.warn(` • ${file.fileName}`);
|
|
81
|
+
});
|
|
82
|
+
console.warn(` Status check skipped - please keep only one script file.`);
|
|
83
|
+
} else if (validation.files.length === 0) {
|
|
84
|
+
console.log(`D ${skillIdn}/ (no script files)`);
|
|
85
|
+
dirty++;
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Get the single valid script file
|
|
91
|
+
const skillFile = await getSingleSkillFile(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn);
|
|
92
|
+
if (!skillFile) {
|
|
93
|
+
console.log(`D ${skillIdn}/ (no valid script file)`);
|
|
94
|
+
dirty++;
|
|
95
|
+
if (verbose) console.log(` ❌ No valid script file found: ${skillIdn}`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const content = skillFile.content;
|
|
100
|
+
const currentPath = skillFile.filePath;
|
|
101
|
+
const h = sha256(content);
|
|
102
|
+
const oldHash = hashes[currentPath];
|
|
103
|
+
|
|
104
|
+
if (verbose) {
|
|
105
|
+
console.log(` 📄 ${currentPath}`);
|
|
106
|
+
console.log(` Old hash: ${oldHash || 'none'}`);
|
|
107
|
+
console.log(` New hash: ${h}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (oldHash !== h) {
|
|
111
|
+
console.log(`M ${currentPath}`);
|
|
112
|
+
dirty++;
|
|
113
|
+
if (verbose) console.log(` 🔄 Modified: ${skillFile.fileName}`);
|
|
114
|
+
} else if (verbose) {
|
|
115
|
+
console.log(` ✓ Unchanged: ${skillFile.fileName}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check metadata.yaml files for changes
|
|
120
|
+
for (const [skillIdn] of Object.entries(flowObj.skills)) {
|
|
121
|
+
const metadataPath = projectIdn ?
|
|
122
|
+
skillMetadataPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn) :
|
|
123
|
+
skillMetadataPath(customer.idn, '', agentIdn, flowIdn, skillIdn);
|
|
124
|
+
|
|
125
|
+
if (await fs.pathExists(metadataPath)) {
|
|
126
|
+
const metadataContent = await fs.readFile(metadataPath, 'utf8');
|
|
127
|
+
const h = sha256(metadataContent);
|
|
128
|
+
const oldHash = hashes[metadataPath];
|
|
129
|
+
|
|
130
|
+
if (verbose) {
|
|
131
|
+
console.log(` 📄 ${metadataPath}`);
|
|
132
|
+
console.log(` Old hash: ${oldHash || 'none'}`);
|
|
133
|
+
console.log(` New hash: ${h}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (oldHash !== h) {
|
|
137
|
+
console.log(`M ${metadataPath}`);
|
|
138
|
+
dirty++;
|
|
139
|
+
|
|
140
|
+
// Show which metadata fields changed
|
|
141
|
+
try {
|
|
142
|
+
const newMetadata = yaml.load(metadataContent) as any;
|
|
143
|
+
console.log(` 📊 Metadata changed for skill: ${skillIdn}`);
|
|
144
|
+
if (newMetadata?.title) {
|
|
145
|
+
console.log(` • Title: ${newMetadata.title}`);
|
|
146
|
+
}
|
|
147
|
+
if (newMetadata?.runner_type) {
|
|
148
|
+
console.log(` • Runner: ${newMetadata.runner_type}`);
|
|
149
|
+
}
|
|
150
|
+
if (newMetadata?.model) {
|
|
151
|
+
console.log(` • Model: ${newMetadata.model.provider_idn}/${newMetadata.model.model_idn}`);
|
|
152
|
+
}
|
|
153
|
+
} catch (e) {
|
|
154
|
+
if (verbose) console.log(` 🔄 Modified: metadata.yaml`);
|
|
155
|
+
}
|
|
156
|
+
} else if (verbose) {
|
|
157
|
+
console.log(` ✓ Unchanged: ${metadataPath}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check attributes file for changes
|
|
166
|
+
try {
|
|
167
|
+
const attributesFile = customerAttributesPath(customer.idn);
|
|
168
|
+
if (await fs.pathExists(attributesFile)) {
|
|
169
|
+
const content = await fs.readFile(attributesFile, 'utf8');
|
|
170
|
+
const h = sha256(content);
|
|
171
|
+
const oldHash = hashes[attributesFile];
|
|
172
|
+
|
|
173
|
+
if (verbose) {
|
|
174
|
+
console.log(`📄 ${attributesFile}`);
|
|
175
|
+
console.log(` Old hash: ${oldHash || 'none'}`);
|
|
176
|
+
console.log(` New hash: ${h}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (oldHash !== h) {
|
|
180
|
+
console.log(`M ${attributesFile}`);
|
|
181
|
+
dirty++;
|
|
182
|
+
|
|
183
|
+
// Show which attributes changed by comparing with backup
|
|
184
|
+
try {
|
|
185
|
+
const attributesBackupFile = customerAttributesBackupPath(customer.idn);
|
|
186
|
+
if (await fs.pathExists(attributesBackupFile)) {
|
|
187
|
+
const backupContent = await fs.readFile(attributesBackupFile, 'utf8');
|
|
188
|
+
|
|
189
|
+
const parseYaml = (content: string) => {
|
|
190
|
+
let yamlContent = content.replace(/!enum "([^"]+)"/g, '"$1"');
|
|
191
|
+
return yaml.load(yamlContent) as { attributes: any[] };
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const currentData = parseYaml(content);
|
|
195
|
+
const backupData = parseYaml(backupContent);
|
|
196
|
+
|
|
197
|
+
if (currentData?.attributes && backupData?.attributes) {
|
|
198
|
+
const currentAttrs = new Map(currentData.attributes.map(attr => [attr.idn, attr]));
|
|
199
|
+
const backupAttrs = new Map(backupData.attributes.map(attr => [attr.idn, attr]));
|
|
200
|
+
|
|
201
|
+
const changedAttributes: string[] = [];
|
|
202
|
+
|
|
203
|
+
for (const [idn, currentAttr] of currentAttrs) {
|
|
204
|
+
const backupAttr = backupAttrs.get(idn);
|
|
205
|
+
const hasChanged = !backupAttr ||
|
|
206
|
+
currentAttr.value !== backupAttr.value ||
|
|
207
|
+
currentAttr.title !== backupAttr.title ||
|
|
208
|
+
currentAttr.description !== backupAttr.description ||
|
|
209
|
+
currentAttr.group !== backupAttr.group ||
|
|
210
|
+
currentAttr.is_hidden !== backupAttr.is_hidden;
|
|
211
|
+
|
|
212
|
+
if (hasChanged) {
|
|
213
|
+
changedAttributes.push(idn);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (changedAttributes.length > 0) {
|
|
218
|
+
console.log(` 📊 Changed attributes (${changedAttributes.length}):`);
|
|
219
|
+
changedAttributes.slice(0, 5).forEach(idn => {
|
|
220
|
+
const current = currentAttrs.get(idn);
|
|
221
|
+
console.log(` • ${idn}: ${current?.title || 'No title'}`);
|
|
222
|
+
});
|
|
223
|
+
if (changedAttributes.length > 5) {
|
|
224
|
+
console.log(` ... and ${changedAttributes.length - 5} more`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch (e) {
|
|
230
|
+
// Fallback to simple message if diff analysis fails
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (verbose) console.log(` 🔄 Modified: attributes.yaml`);
|
|
234
|
+
} else if (verbose) {
|
|
235
|
+
console.log(` ✓ Unchanged: attributes.yaml`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (verbose) console.log(`⚠️ Error checking attributes: ${error instanceof Error ? error.message : String(error)}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Check flows.yaml file for changes
|
|
243
|
+
const flowsFile = flowsYamlPath(customer.idn);
|
|
244
|
+
if (await fs.pathExists(flowsFile)) {
|
|
245
|
+
try {
|
|
246
|
+
const flowsContent = await fs.readFile(flowsFile, 'utf8');
|
|
247
|
+
const h = sha256(flowsContent);
|
|
248
|
+
const oldHash = hashes[flowsFile];
|
|
249
|
+
|
|
250
|
+
if (verbose) {
|
|
251
|
+
console.log(`📄 flows.yaml`);
|
|
252
|
+
console.log(` Old hash: ${oldHash || 'none'}`);
|
|
253
|
+
console.log(` New hash: ${h}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (oldHash !== h) {
|
|
257
|
+
console.log(`M ${flowsFile}`);
|
|
258
|
+
dirty++;
|
|
259
|
+
if (verbose) {
|
|
260
|
+
const flowsStats = await fs.stat(flowsFile);
|
|
261
|
+
console.log(` 🔄 Modified: flows.yaml`);
|
|
262
|
+
console.log(` 📊 Size: ${(flowsStats.size / 1024).toFixed(1)}KB`);
|
|
263
|
+
console.log(` 📅 Last modified: ${flowsStats.mtime.toISOString()}`);
|
|
264
|
+
}
|
|
265
|
+
} else if (verbose) {
|
|
266
|
+
const flowsStats = await fs.stat(flowsFile);
|
|
267
|
+
console.log(` ✓ Unchanged: flows.yaml`);
|
|
268
|
+
console.log(` 📅 Last modified: ${flowsStats.mtime.toISOString()}`);
|
|
269
|
+
console.log(` 📊 Size: ${(flowsStats.size / 1024).toFixed(1)}KB`);
|
|
270
|
+
}
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (verbose) console.log(`⚠️ Error checking flows.yaml: ${error instanceof Error ? error.message : String(error)}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.log(dirty ? `${dirty} changed file(s).` : 'Clean.');
|
|
277
|
+
}
|