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
426 lines • 21.9 kB
JavaScript
/**
* Status checking module
*/
import fs from 'fs-extra';
import yaml from 'js-yaml';
import { sha256, loadHashes } from '../hash.js';
import { ensureState, mapPath, skillMetadataPath, customerAttributesPath, customerAttributesBackupPath, flowsYamlPath, projectDir, agentMetadataPath, flowMetadataPath } from '../fsutil.js';
import { validateSkillFolder, getSingleSkillFile } from './skill-files.js';
// Type guards for project map formats
function isProjectMap(x) {
return typeof x === 'object' && x !== null && 'projects' in x;
}
function isLegacyProjectMap(x) {
return typeof x === 'object' && x !== null && 'projectId' in x && 'agents' in x;
}
/**
* Scan filesystem for local-only entities not in the project map yet
*/
async function scanForLocalOnlyEntities(customer, projects, verbose = false) {
const localEntities = [];
let agentCount = 0;
let flowCount = 0;
let skillCount = 0;
// Scan each project directory
for (const [projectIdn] of Object.entries(projects)) {
const projDir = projectDir(customer.idn, projectIdn);
if (!(await fs.pathExists(projDir)))
continue;
if (verbose)
console.log(`🔍 Scanning project directory: ${projDir}`);
// Get all subdirectories in the project (these should be agents)
const agentDirs = await fs.readdir(projDir);
for (const agentIdn of agentDirs) {
const agentPath = `${projDir}/${agentIdn}`;
const agentStat = await fs.stat(agentPath);
// Skip files, only process directories
if (!agentStat.isDirectory())
continue;
// Skip if it's not really an agent directory (no metadata.yaml)
const agentMetaPath = agentMetadataPath(customer.idn, projectIdn, agentIdn);
if (!(await fs.pathExists(agentMetaPath)))
continue;
// Check if this agent is already in the project map
const projectData = projects[projectIdn];
if (!projectData?.agents[agentIdn]) {
// This is a local-only agent!
localEntities.push({
type: 'agent',
path: agentMetaPath,
idn: agentIdn,
projectIdn
});
agentCount++;
if (verbose)
console.log(` 🆕 Found local-only agent: ${agentIdn}`);
}
// Now scan for flows within this agent (regardless of whether agent is local-only or not)
try {
const flowDirs = await fs.readdir(agentPath);
for (const flowIdn of flowDirs) {
const flowPath = `${agentPath}/${flowIdn}`;
const flowStat = await fs.stat(flowPath);
// Skip files, only process directories
if (!flowStat.isDirectory())
continue;
// Skip if it's not really a flow directory (no metadata.yaml)
const flowMetaPath = flowMetadataPath(customer.idn, projectIdn, agentIdn, flowIdn);
if (!(await fs.pathExists(flowMetaPath)))
continue;
// Check if this flow exists in the project map
const agentData = projectData?.agents[agentIdn];
if (!agentData?.flows[flowIdn]) {
// This is a local-only flow!
localEntities.push({
type: 'flow',
path: flowMetaPath,
idn: flowIdn,
projectIdn,
agentIdn
});
flowCount++;
if (verbose)
console.log(` 🆕 Found local-only flow: ${agentIdn}/${flowIdn}`);
}
// Now scan for skills within this flow (regardless of whether flow is local-only or not)
try {
const skillDirs = await fs.readdir(flowPath);
for (const skillIdn of skillDirs) {
const skillPath = `${flowPath}/${skillIdn}`;
const skillStat = await fs.stat(skillPath);
// Skip files, only process directories
if (!skillStat.isDirectory())
continue;
// Skip if it's not really a skill directory (no metadata.yaml)
const skillMetaPath = skillMetadataPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn);
if (!(await fs.pathExists(skillMetaPath)))
continue;
// Check if this skill exists in the project map
const flowData = agentData?.flows[flowIdn];
if (!flowData?.skills[skillIdn]) {
// This is a local-only skill!
localEntities.push({
type: 'skill',
path: skillMetaPath,
idn: skillIdn,
projectIdn,
agentIdn,
flowIdn
});
skillCount++;
if (verbose)
console.log(` 🆕 Found local-only skill: ${agentIdn}/${flowIdn}/${skillIdn}`);
}
}
}
catch (error) {
// Ignore errors reading flow directory
}
}
}
catch (error) {
// Ignore errors reading agent directory
}
}
}
return { agentCount, flowCount, skillCount, entities: localEntities };
}
/**
* Check status of files for a customer
*/
export async function status(customer, verbose = false) {
await ensureState(customer.idn);
if (!(await fs.pathExists(mapPath(customer.idn)))) {
console.log(`No map for customer ${customer.idn}. Run \`newo pull --customer ${customer.idn}\` first.`);
return;
}
if (verbose)
console.log(`📋 Loading project mapping and hashes for customer ${customer.idn}...`);
const idMapData = await fs.readJson(mapPath(customer.idn));
const hashes = await loadHashes(customer.idn);
let dirty = 0;
// Handle both old single-project format and new multi-project format with type guards
const projects = isProjectMap(idMapData) && idMapData.projects
? idMapData.projects
: isLegacyProjectMap(idMapData)
? { '': idMapData }
: (() => { throw new Error('Invalid project map format'); })();
// First, scan for any local-only entities (created locally but not yet pushed)
const localScan = await scanForLocalOnlyEntities(customer, projects, verbose);
const totalLocalEntities = localScan.agentCount + localScan.flowCount + localScan.skillCount;
if (totalLocalEntities > 0) {
dirty += totalLocalEntities;
for (const entity of localScan.entities) {
if (entity.type === 'agent') {
console.log(`A ${entity.projectIdn}/${entity.idn}/metadata.yaml (new agent)`);
if (verbose) {
try {
const metadataContent = await fs.readFile(entity.path, 'utf8');
const metadata = yaml.load(metadataContent);
console.log(` 📊 New Agent: ${entity.idn}`);
if (metadata?.title && metadata.title !== entity.idn) {
console.log(` • Title: ${metadata.title}`);
}
if (metadata?.description) {
console.log(` • Description: ${metadata.description}`);
}
}
catch (e) {
// Ignore parsing errors
}
}
}
else if (entity.type === 'flow') {
console.log(`A ${entity.projectIdn}/${entity.agentIdn}/${entity.idn}/metadata.yaml (new flow)`);
if (verbose) {
try {
const metadataContent = await fs.readFile(entity.path, 'utf8');
const metadata = yaml.load(metadataContent);
console.log(` 📊 New Flow: ${entity.idn}`);
if (metadata?.title && metadata.title !== entity.idn) {
console.log(` • Title: ${metadata.title}`);
}
if (metadata?.default_runner_type) {
console.log(` • Runner: ${metadata.default_runner_type}`);
}
}
catch (e) {
// Ignore parsing errors
}
}
}
else if (entity.type === 'skill') {
console.log(`A ${entity.projectIdn}/${entity.agentIdn}/${entity.flowIdn}/${entity.idn}/metadata.yaml (new skill)`);
if (verbose) {
try {
const metadataContent = await fs.readFile(entity.path, 'utf8');
const metadata = yaml.load(metadataContent);
console.log(` 📊 New Skill: ${entity.idn}`);
if (metadata?.title && metadata.title !== entity.idn) {
console.log(` • Title: ${metadata.title}`);
}
if (metadata?.runner_type) {
console.log(` • Runner: ${metadata.runner_type}`);
}
}
catch (e) {
// Ignore parsing errors
}
}
}
}
}
for (const [projectIdn, projectData] of Object.entries(projects)) {
if (verbose && projectIdn)
console.log(`📁 Checking project: ${projectIdn}`);
for (const [agentIdn, agentObj] of Object.entries(projectData.agents)) {
if (verbose)
console.log(` 📁 Checking agent: ${agentIdn}`);
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
if (verbose)
console.log(` 📁 Checking flow: ${flowIdn}`);
for (const [skillIdn] of Object.entries(flowObj.skills)) {
// Validate skill folder and show warnings
const validation = await validateSkillFolder(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn);
if (!validation.isValid) {
// Show warnings and errors
validation.errors.forEach(error => {
console.error(`❌ ${error}`);
});
validation.warnings.forEach(warning => {
console.warn(`⚠️ ${warning}`);
});
if (validation.files.length > 1) {
console.warn(`⚠️ Multiple script files in skill ${skillIdn}:`);
validation.files.forEach(file => {
console.warn(` • ${file.fileName}`);
});
console.warn(` Status check skipped - please keep only one script file.`);
}
else if (validation.files.length === 0) {
const displayPath = projectIdn ? `${projectIdn}/${agentIdn}/${flowIdn}/${skillIdn}` : `${agentIdn}/${flowIdn}/${skillIdn}`;
console.log(`D ${displayPath}/ (no script files)`);
dirty++;
}
continue;
}
// Get the single valid script file
const skillFile = await getSingleSkillFile(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn);
if (!skillFile) {
const displayPath = projectIdn ? `${projectIdn}/${agentIdn}/${flowIdn}/${skillIdn}` : `${agentIdn}/${flowIdn}/${skillIdn}`;
console.log(`D ${displayPath}/ (no valid script file)`);
dirty++;
if (verbose)
console.log(` ❌ No valid script file found: ${skillIdn}`);
continue;
}
const content = skillFile.content;
const currentPath = skillFile.filePath;
const h = sha256(content);
const oldHash = hashes[currentPath];
if (verbose) {
console.log(` 📄 ${currentPath}`);
console.log(` Old hash: ${oldHash || 'none'}`);
console.log(` New hash: ${h}`);
}
if (oldHash !== h) {
console.log(`M ${currentPath}`);
dirty++;
if (verbose)
console.log(` 🔄 Modified: ${skillFile.fileName}`);
}
else if (verbose) {
console.log(` ✓ Unchanged: ${skillFile.fileName}`);
}
}
// Check metadata.yaml files for changes
for (const [skillIdn] of Object.entries(flowObj.skills)) {
const metadataPath = projectIdn ?
skillMetadataPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn) :
skillMetadataPath(customer.idn, '', agentIdn, flowIdn, skillIdn);
if (await fs.pathExists(metadataPath)) {
const metadataContent = await fs.readFile(metadataPath, 'utf8');
const h = sha256(metadataContent);
const oldHash = hashes[metadataPath];
if (verbose) {
console.log(` 📄 ${metadataPath}`);
console.log(` Old hash: ${oldHash || 'none'}`);
console.log(` New hash: ${h}`);
}
if (oldHash !== h) {
console.log(`M ${metadataPath}`);
dirty++;
// Show which metadata fields changed
try {
const newMetadata = yaml.load(metadataContent);
console.log(` 📊 Metadata changed for skill: ${skillIdn}`);
if (newMetadata?.title) {
console.log(` • Title: ${newMetadata.title}`);
}
if (newMetadata?.runner_type) {
console.log(` • Runner: ${newMetadata.runner_type}`);
}
if (newMetadata?.model) {
console.log(` • Model: ${newMetadata.model.provider_idn}/${newMetadata.model.model_idn}`);
}
}
catch (e) {
if (verbose)
console.log(` 🔄 Modified: metadata.yaml`);
}
}
else if (verbose) {
console.log(` ✓ Unchanged: ${metadataPath}`);
}
}
}
}
}
}
// Check attributes file for changes
try {
const attributesFile = customerAttributesPath(customer.idn);
if (await fs.pathExists(attributesFile)) {
const content = await fs.readFile(attributesFile, 'utf8');
const h = sha256(content);
const oldHash = hashes[attributesFile];
if (verbose) {
console.log(`📄 ${attributesFile}`);
console.log(` Old hash: ${oldHash || 'none'}`);
console.log(` New hash: ${h}`);
}
if (oldHash !== h) {
console.log(`M ${attributesFile}`);
dirty++;
// Show which attributes changed by comparing with backup
try {
const attributesBackupFile = customerAttributesBackupPath(customer.idn);
if (await fs.pathExists(attributesBackupFile)) {
const backupContent = await fs.readFile(attributesBackupFile, 'utf8');
const parseYaml = (content) => {
let yamlContent = content.replace(/!enum "([^"]+)"/g, '"$1"');
return yaml.load(yamlContent);
};
const currentData = parseYaml(content);
const backupData = parseYaml(backupContent);
if (currentData?.attributes && backupData?.attributes) {
const currentAttrs = new Map(currentData.attributes.map(attr => [attr.idn, attr]));
const backupAttrs = new Map(backupData.attributes.map(attr => [attr.idn, attr]));
const changedAttributes = [];
for (const [idn, currentAttr] of currentAttrs) {
const backupAttr = backupAttrs.get(idn);
const hasChanged = !backupAttr ||
currentAttr.value !== backupAttr.value ||
currentAttr.title !== backupAttr.title ||
currentAttr.description !== backupAttr.description ||
currentAttr.group !== backupAttr.group ||
currentAttr.is_hidden !== backupAttr.is_hidden;
if (hasChanged) {
changedAttributes.push(idn);
}
}
if (changedAttributes.length > 0) {
console.log(` 📊 Changed attributes (${changedAttributes.length}):`);
changedAttributes.slice(0, 5).forEach(idn => {
const current = currentAttrs.get(idn);
console.log(` • ${idn}: ${current?.title || 'No title'}`);
});
if (changedAttributes.length > 5) {
console.log(` ... and ${changedAttributes.length - 5} more`);
}
}
}
}
}
catch (e) {
// Fallback to simple message if diff analysis fails
}
if (verbose)
console.log(` 🔄 Modified: attributes.yaml`);
}
else if (verbose) {
console.log(` ✓ Unchanged: attributes.yaml`);
}
}
}
catch (error) {
if (verbose)
console.log(`⚠️ Error checking attributes: ${error instanceof Error ? error.message : String(error)}`);
}
// Check flows.yaml file for changes
const flowsFile = flowsYamlPath(customer.idn);
if (await fs.pathExists(flowsFile)) {
try {
const flowsContent = await fs.readFile(flowsFile, 'utf8');
const h = sha256(flowsContent);
const oldHash = hashes[flowsFile];
if (verbose) {
console.log(`📄 flows.yaml`);
console.log(` Old hash: ${oldHash || 'none'}`);
console.log(` New hash: ${h}`);
}
if (oldHash !== h) {
console.log(`M ${flowsFile}`);
dirty++;
if (verbose) {
const flowsStats = await fs.stat(flowsFile);
console.log(` 🔄 Modified: flows.yaml`);
console.log(` 📊 Size: ${(flowsStats.size / 1024).toFixed(1)}KB`);
console.log(` 📅 Last modified: ${flowsStats.mtime.toISOString()}`);
}
}
else if (verbose) {
const flowsStats = await fs.stat(flowsFile);
console.log(` ✓ Unchanged: flows.yaml`);
console.log(` 📅 Last modified: ${flowsStats.mtime.toISOString()}`);
console.log(` 📊 Size: ${(flowsStats.size / 1024).toFixed(1)}KB`);
}
}
catch (error) {
if (verbose)
console.log(`⚠️ Error checking flows.yaml: ${error instanceof Error ? error.message : String(error)}`);
}
}
console.log(dirty ? `${dirty} changed file(s).` : 'Clean.');
}
//# sourceMappingURL=status.js.map