newo
Version:
NEWO CLI: Professional command-line tool with modular architecture for NEWO AI Agent development. Features account migration, integration management, webhook automation, AKB knowledge base, project attributes, sandbox testing, IDN-based file management, r
309 lines ⢠14.1 kB
JavaScript
/**
* Customer and project attributes synchronization module
*/
import { getCustomerAttributes, getProjectAttributes, listProjects, updateProjectAttribute } from '../api.js';
import { writeFileSafe, customerAttributesPath, customerAttributesMapPath, customerAttributesBackupPath } from '../fsutil.js';
import path from 'path';
import fs from 'fs-extra';
import yaml from 'js-yaml';
/**
* Save customer attributes to YAML format and return content for hashing
*/
export async function saveCustomerAttributes(client, customer, verbose = false) {
if (verbose)
console.log(`š Fetching customer attributes for ${customer.idn}...`);
try {
const response = await getCustomerAttributes(client, true); // Include hidden attributes
// API returns { groups: [...], attributes: [...] }
// We only want the attributes array in the expected format
const attributes = response.attributes || response;
if (verbose)
console.log(`š¦ Found ${Array.isArray(attributes) ? attributes.length : 'invalid'} attributes`);
// Create ID mapping for push operations (separate from YAML)
const idMapping = {};
// Transform attributes to match reference format exactly (no ID fields)
const cleanAttributes = Array.isArray(attributes) ? attributes.map(attr => {
// Store ID mapping for push operations
if (attr.id) {
idMapping[attr.idn] = attr.id;
}
// Special handling for complex JSON string values
let processedValue = attr.value;
if (typeof attr.value === 'string' && attr.value.startsWith('[{') && attr.value.endsWith('}]')) {
try {
// Parse and reformat JSON for better readability
const parsed = JSON.parse(attr.value);
processedValue = JSON.stringify(parsed, null, 0); // No extra spacing, but valid JSON
}
catch (e) {
// Keep original if parsing fails
processedValue = attr.value;
}
}
const cleanAttr = {
idn: attr.idn,
value: processedValue,
title: attr.title || "",
description: attr.description || "",
group: attr.group || "",
is_hidden: attr.is_hidden,
possible_values: attr.possible_values || [],
value_type: `__ENUM_PLACEHOLDER_${attr.value_type}__`
};
return cleanAttr;
}) : [];
const attributesYaml = {
attributes: cleanAttributes
};
// Configure YAML output to match reference format exactly
let yamlContent = yaml.dump(attributesYaml, {
indent: 2,
quotingType: '"',
forceQuotes: false,
lineWidth: 80, // Wrap long lines to match reference format
noRefs: true,
sortKeys: false,
flowLevel: -1, // Never use flow syntax
styles: {
'!!str': 'folded' // Use folded style for better line wrapping of long strings
}
});
// Post-process to fix enum format and improve JSON string formatting
yamlContent = yamlContent.replace(/__ENUM_PLACEHOLDER_(\w+)__/g, '!enum "AttributeValueTypes.$1"');
// Fix JSON string formatting to match reference (remove escape characters)
yamlContent = yamlContent.replace(/\\"/g, '"');
// Save all files: attributes.yaml, ID mapping, and backup for diff tracking
await writeFileSafe(customerAttributesPath(customer.idn), yamlContent);
await writeFileSafe(customerAttributesMapPath(customer.idn), JSON.stringify(idMapping, null, 2));
await writeFileSafe(customerAttributesBackupPath(customer.idn), yamlContent);
if (verbose) {
console.log(`ā Saved customer attributes to ${customerAttributesPath(customer.idn)}`);
console.log(`ā Saved attribute ID mapping to ${customerAttributesMapPath(customer.idn)}`);
console.log(`ā Created attributes backup for diff tracking`);
}
// Return content for hash calculation
return yamlContent;
}
catch (error) {
console.error(`ā Failed to save customer attributes for ${customer.idn}:`, error);
throw error;
}
}
/**
* Save project attributes to YAML format in project directory
*/
export async function saveProjectAttributes(client, customer, projectId, projectIdn, verbose = false) {
if (verbose)
console.log(` š Fetching project attributes for ${projectIdn}...`);
try {
const response = await getProjectAttributes(client, projectId, true); // Include hidden attributes
// API returns { groups: [...], attributes: [...] }
const attributes = response.attributes || response;
if (verbose)
console.log(` š¦ Found ${Array.isArray(attributes) ? attributes.length : 0} project attributes`);
if (!Array.isArray(attributes) || attributes.length === 0) {
if (verbose)
console.log(` ā¹ No project attributes found for ${projectIdn}`);
return;
}
// Create ID mapping for push operations
const idMapping = {};
// Transform attributes to match format (no ID fields)
const cleanAttributes = attributes.map(attr => {
// Store ID mapping
if (attr.id) {
idMapping[attr.idn] = attr.id;
}
// Special handling for complex JSON string values
let processedValue = attr.value;
if (typeof attr.value === 'string' && attr.value.startsWith('[{') && attr.value.endsWith('}]')) {
try {
const parsed = JSON.parse(attr.value);
processedValue = JSON.stringify(parsed, null, 0);
}
catch (e) {
processedValue = attr.value;
}
}
return {
idn: attr.idn,
value: processedValue,
title: attr.title || "",
description: attr.description || "",
group: attr.group || "",
is_hidden: attr.is_hidden,
possible_values: attr.possible_values || [],
value_type: `__ENUM_PLACEHOLDER_${attr.value_type}__`
};
});
const attributesYaml = {
attributes: cleanAttributes
};
// Configure YAML output
let yamlContent = yaml.dump(attributesYaml, {
indent: 2,
quotingType: '"',
forceQuotes: false,
lineWidth: 80,
noRefs: true,
sortKeys: false,
flowLevel: -1
});
// Post-process to fix enum format
yamlContent = yamlContent.replace(/__ENUM_PLACEHOLDER_(\w+)__/g, '!enum "AttributeValueTypes.$1"');
yamlContent = yamlContent.replace(/\\"/g, '"');
// Save to project directory
const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn);
const projectDir = path.join(customerDir, 'projects', projectIdn);
await fs.ensureDir(projectDir);
const attributesFile = path.join(projectDir, 'attributes.yaml');
const attributesMapFile = path.join(customerDir, '.newo', customer.idn, `project_${projectIdn}_attributes-map.json`);
await writeFileSafe(attributesFile, yamlContent);
await fs.ensureDir(path.dirname(attributesMapFile));
await writeFileSafe(attributesMapFile, JSON.stringify(idMapping, null, 2));
if (verbose) {
console.log(` ā Saved project attributes to projects/${projectIdn}/attributes.yaml`);
console.log(` ā Saved attribute ID mapping`);
}
}
catch (error) {
if (verbose)
console.error(` ā Failed to fetch project attributes for ${projectIdn}:`, error.message);
}
}
/**
* Pull all project attributes for a customer
*/
export async function pullAllProjectAttributes(client, customer, verbose = false) {
if (verbose)
console.log(`\nš Fetching project attributes...`);
try {
// Get all projects for this customer
const projects = await listProjects(client);
if (verbose)
console.log(`ā Found ${projects.length} projects\n`);
for (const project of projects) {
await saveProjectAttributes(client, customer, project.id, project.idn, verbose);
}
if (verbose)
console.log(`\nā
Completed project attributes sync for ${projects.length} projects\n`);
}
catch (error) {
console.error(`ā Failed to pull project attributes:`, error);
throw error;
}
}
/**
* Push modified project attributes for a specific project
*/
export async function pushProjectAttributes(client, customer, projectId, projectIdn, verbose = false) {
const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn);
const attributesFile = path.join(customerDir, 'projects', projectIdn, 'attributes.yaml');
const attributesMapFile = path.join(customerDir, '.newo', customer.idn, `project_${projectIdn}_attributes-map.json`);
// Check if attributes file exists
if (!await fs.pathExists(attributesFile)) {
if (verbose)
console.log(` ā¹ No project attributes file for ${projectIdn}`);
return 0;
}
// Load local attributes
// Read as text and replace the custom !enum tags before parsing
let attributesContent = await fs.readFile(attributesFile, 'utf-8');
// Replace custom enum tags with the actual value
attributesContent = attributesContent.replace(/!enum "AttributeValueTypes\.(\w+)"/g, '$1');
const localData = yaml.load(attributesContent);
const localAttributes = localData.attributes || [];
// Load ID mapping
if (!await fs.pathExists(attributesMapFile)) {
if (verbose)
console.log(` ā No ID mapping found for project ${projectIdn}, skipping push`);
return 0;
}
const idMapping = JSON.parse(await fs.readFile(attributesMapFile, 'utf-8'));
// Fetch current remote attributes for comparison
const remoteResponse = await getProjectAttributes(client, projectId, true);
const remoteAttributes = remoteResponse.attributes || [];
// Create map of remote attributes by IDN
const remoteMap = new Map();
remoteAttributes.forEach(attr => remoteMap.set(attr.idn, attr));
let updatedCount = 0;
// Check each local attribute for changes
for (const localAttr of localAttributes) {
const attributeId = idMapping[localAttr.idn];
if (!attributeId) {
if (verbose)
console.log(` ā No ID mapping for attribute ${localAttr.idn}, skipping`);
continue;
}
const remoteAttr = remoteMap.get(localAttr.idn);
if (!remoteAttr) {
if (verbose)
console.log(` ā Attribute ${localAttr.idn} not found remotely, skipping`);
continue;
}
// Value type is already parsed (we removed !enum tags above)
const valueType = localAttr.value_type;
// Check if value changed
const localValue = String(localAttr.value || '');
const remoteValue = String(remoteAttr.value || '');
if (localValue !== remoteValue) {
if (verbose)
console.log(` š Updating project attribute: ${localAttr.idn}`);
try {
const attributeToUpdate = {
id: attributeId,
idn: localAttr.idn,
value: localAttr.value,
title: localAttr.title,
description: localAttr.description,
group: localAttr.group,
is_hidden: localAttr.is_hidden,
possible_values: localAttr.possible_values,
value_type: valueType
};
await updateProjectAttribute(client, projectId, attributeToUpdate);
if (verbose)
console.log(` ā
Updated: ${localAttr.idn} (${localAttr.title})`);
updatedCount++;
}
catch (error) {
const errorDetail = error.response?.data || error.message;
console.error(` ā Failed to update ${localAttr.idn}: ${JSON.stringify(errorDetail)}`);
if (verbose) {
console.error(` API response:`, error.response?.status, error.response?.statusText);
console.error(` Endpoint tried: PUT /api/v1/project/attributes/${attributeId}`);
}
}
}
else if (verbose) {
console.log(` ā No changes: ${localAttr.idn}`);
}
}
return updatedCount;
}
/**
* Push all modified project attributes for all projects
*/
export async function pushAllProjectAttributes(client, customer, projectsMap, verbose = false) {
if (verbose)
console.log(`\nš Checking project attributes for changes...`);
let totalUpdated = 0;
for (const [projectIdn, projectInfo] of Object.entries(projectsMap)) {
if (!projectIdn)
continue; // Skip empty project idn (legacy format)
if (verbose)
console.log(`\n š Project: ${projectIdn}`);
const updated = await pushProjectAttributes(client, customer, projectInfo.projectId, projectInfo.projectIdn || projectIdn, verbose);
totalUpdated += updated;
}
// Always show summary if changes were made
if (totalUpdated > 0) {
console.log(`\nā
Updated ${totalUpdated} project attribute(s)`);
}
else {
if (verbose)
console.log(`\nā No project attribute changes to push`);
}
return totalUpdated;
}
//# sourceMappingURL=attributes.js.map