UNPKG

newo

Version:

NEWO CLI: Professional command-line tool with modular architecture for NEWO AI Agent development. Features IDN-based file management, real-time progress tracking, intelligent sync operations, and comprehensive multi-customer support.

217 lines (185 loc) 6.39 kB
/** * Skill file management utilities */ import fs from 'fs-extra'; import path from 'path'; import { sha256 } from '../hash.js'; import { skillFolderPath } from '../fsutil.js'; import type { RunnerType } from '../types.js'; export interface SkillFile { filePath: string; fileName: string; extension: string; content: string; } export interface SkillFileValidation { isValid: boolean; files: SkillFile[]; warnings: string[]; errors: string[]; } /** * Get the correct file extension for a runner type */ export function getExtensionForRunner(runnerType: RunnerType): string { switch (runnerType) { case 'guidance': return 'guidance'; case 'nsl': return 'jinja'; default: return 'guidance'; } } /** * Generate IDN-based script file path */ export function getIdnBasedScriptPath( customerIdn: string, projectIdn: string, agentIdn: string, flowIdn: string, skillIdn: string, runnerType: RunnerType ): string { const extension = getExtensionForRunner(runnerType); const folderPath = skillFolderPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn); return path.join(folderPath, `${skillIdn}.${extension}`); } /** * Find all script files in a skill folder */ export async function findSkillScriptFiles(skillFolderPath: string): Promise<SkillFile[]> { if (!(await fs.pathExists(skillFolderPath))) { return []; } const files = await fs.readdir(skillFolderPath); const scriptFiles: SkillFile[] = []; for (const fileName of files) { const filePath = path.join(skillFolderPath, fileName); const stats = await fs.stat(filePath); if (stats.isFile()) { const ext = path.extname(fileName).toLowerCase(); // Check for script file extensions if (['.jinja', '.guidance', '.nsl'].includes(ext)) { const content = await fs.readFile(filePath, 'utf8'); scriptFiles.push({ filePath, fileName, extension: ext.slice(1), // Remove the dot content }); } } } return scriptFiles; } /** * Validate skill folder has exactly one script file */ export async function validateSkillFolder( customerIdn: string, projectIdn: string, agentIdn: string, flowIdn: string, skillIdn: string ): Promise<SkillFileValidation> { const folderPath = skillFolderPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn); const files = await findSkillScriptFiles(folderPath); const warnings: string[] = []; const errors: string[] = []; if (files.length === 0) { errors.push(`No script files found in skill folder: ${skillIdn}`); } else if (files.length > 1) { errors.push(`Multiple script files found in skill ${skillIdn}: ${files.map(f => f.fileName).join(', ')}`); warnings.push(`Only one script file allowed per skill. Remove extra files and keep one.`); } return { isValid: files.length === 1, files, warnings, errors }; } /** * Get the single skill script file (if valid) */ export async function getSingleSkillFile( customerIdn: string, projectIdn: string, agentIdn: string, flowIdn: string, skillIdn: string ): Promise<SkillFile | null> { const validation = await validateSkillFolder(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn); if (validation.isValid && validation.files.length === 1) { return validation.files[0]!; } return null; } /** * Check if skill script content is different from target content */ export function isContentDifferent(existingContent: string, newContent: string): boolean { return sha256(existingContent.trim()) !== sha256(newContent.trim()); } export type OverwriteChoice = 'yes' | 'no' | 'all' | 'quit'; /** * Interactive overwrite confirmation with content diff */ export async function askForOverwrite(skillIdn: string, existingContent: string, newContent: string, fileName: string): Promise<OverwriteChoice> { const readline = await import('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); console.log(`\n⚠️ Local changes will be replaced by remote content for skill ${skillIdn} (${fileName}):`); // ANSI color codes matching GitHub diff colors from screenshot const redBg = '\x1b[101m\x1b[97m'; // Light red background, white text (like GitHub) const greenBg = '\x1b[102m\x1b[30m'; // Light green background, black text (like GitHub) const gray = '\x1b[90m'; const reset = '\x1b[0m'; // Generate proper diff using LCS algorithm const { generateDiff, filterDiffWithContext } = await import('./diff-utils.js'); const fullDiff = generateDiff(existingContent, newContent); const contextDiff = filterDiffWithContext(fullDiff, 2); if (contextDiff.length === 0) { console.log(`${gray} No differences found${reset}`); return 'no'; } // Display the diff with proper GitHub-style formatting for (const line of contextDiff) { if (line.type === 'context') { // Show context lines in gray const lineNum = line.localLineNum !== -1 ? line.localLineNum : line.remoteLineNum; console.log(` ${String(lineNum).padStart(3)} ${line.content}`); } else if (line.type === 'remove') { // Show local content being removed (red background) console.log(`${redBg} - ${String(line.localLineNum).padStart(3)} ${line.content} ${reset}`); } else if (line.type === 'add') { // Show remote content being added (green background) console.log(`${greenBg} + ${String(line.remoteLineNum).padStart(3)} ${line.content} ${reset}`); } } // Show if there are more changes beyond what we're displaying const totalChanges = fullDiff.filter(line => line.type !== 'context').length; const displayedChanges = contextDiff.filter(line => line.type !== 'context').length; if (totalChanges > displayedChanges) { console.log(`${gray}... (${totalChanges - displayedChanges} more changes)${reset}`); } const answer = await new Promise<string>((resolve) => { rl.question(`\nReplace local with remote? (y)es/(n)o/(a)ll/(q)uit: `, resolve); }); rl.close(); const choice = answer.toLowerCase().trim(); if (choice === 'q' || choice === 'quit') { return 'quit'; } if (choice === 'a' || choice === 'all') { return 'all'; } if (choice === 'y' || choice === 'yes') { return 'yes'; } return 'no'; }