Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
378
vendor/ruvector/npm/packages/rudag/bin/cli.js
vendored
Normal file
378
vendor/ruvector/npm/packages/rudag/bin/cli.js
vendored
Normal file
@@ -0,0 +1,378 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* rudag CLI - Command-line interface for DAG operations
|
||||
*
|
||||
* @security File paths are validated to prevent reading arbitrary files
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Lazy load to improve startup time
|
||||
let RuDag, DagOperator, AttentionMechanism;
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
|
||||
const help = `
|
||||
rudag - Self-learning DAG query optimization CLI
|
||||
|
||||
Usage: rudag <command> [options]
|
||||
|
||||
Commands:
|
||||
create <name> Create a new DAG and output to stdout
|
||||
load <file> Load DAG from file (must be .dag or .json)
|
||||
info <file> Show DAG information
|
||||
topo <file> Print topological sort
|
||||
critical <file> Find critical path
|
||||
attention <file> [type] Compute attention scores (type: topo|critical|uniform)
|
||||
convert <in> <out> Convert between JSON and binary formats
|
||||
help Show this help message
|
||||
|
||||
Examples:
|
||||
rudag create my-query > my-query.dag
|
||||
rudag info ./data/my-query.dag
|
||||
rudag critical ./queries/query.dag
|
||||
rudag attention query.dag critical
|
||||
|
||||
Options:
|
||||
--json Output in JSON format
|
||||
--verbose Verbose output
|
||||
|
||||
Security:
|
||||
- Only .dag and .json files are allowed
|
||||
- Paths are restricted to current directory and subdirectories
|
||||
`;
|
||||
|
||||
/**
|
||||
* Validate file path for security
|
||||
* @security Prevents path traversal and restricts to allowed extensions
|
||||
*/
|
||||
function validateFilePath(filePath) {
|
||||
if (!filePath || typeof filePath !== 'string') {
|
||||
throw new Error('File path is required');
|
||||
}
|
||||
|
||||
// Check extension
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
if (ext !== '.dag' && ext !== '.json') {
|
||||
throw new Error(`Invalid file extension: ${ext}. Only .dag and .json files are allowed.`);
|
||||
}
|
||||
|
||||
// Resolve to absolute path
|
||||
const resolved = path.resolve(filePath);
|
||||
const cwd = process.cwd();
|
||||
|
||||
// Ensure path is within current directory (prevents traversal)
|
||||
if (!resolved.startsWith(cwd + path.sep) && resolved !== cwd) {
|
||||
// Allow absolute paths within cwd or relative paths
|
||||
const normalized = path.normalize(filePath);
|
||||
if (normalized.startsWith('..') || path.isAbsolute(normalized)) {
|
||||
// Check if absolute path is within cwd
|
||||
if (!resolved.startsWith(cwd)) {
|
||||
throw new Error('Access denied: file path must be within current directory');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Additional check: no null bytes
|
||||
if (filePath.includes('\0')) {
|
||||
throw new Error('Invalid file path: contains null bytes');
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate output file path
|
||||
*/
|
||||
function validateOutputPath(filePath) {
|
||||
if (!filePath || typeof filePath !== 'string') {
|
||||
throw new Error('Output file path is required');
|
||||
}
|
||||
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
if (ext !== '.dag' && ext !== '.json') {
|
||||
throw new Error(`Invalid output extension: ${ext}. Only .dag and .json files are allowed.`);
|
||||
}
|
||||
|
||||
const resolved = path.resolve(filePath);
|
||||
const cwd = process.cwd();
|
||||
|
||||
// Must be within current directory
|
||||
if (!resolved.startsWith(cwd + path.sep) && resolved !== cwd) {
|
||||
throw new Error('Access denied: output path must be within current directory');
|
||||
}
|
||||
|
||||
if (filePath.includes('\0')) {
|
||||
throw new Error('Invalid file path: contains null bytes');
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy load dependencies
|
||||
*/
|
||||
async function loadDependencies() {
|
||||
if (!RuDag) {
|
||||
const mod = require('../dist/index.js');
|
||||
RuDag = mod.RuDag;
|
||||
DagOperator = mod.DagOperator;
|
||||
AttentionMechanism = mod.AttentionMechanism;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!command || command === 'help' || command === '--help') {
|
||||
console.log(help);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const isJson = args.includes('--json');
|
||||
const verbose = args.includes('--verbose');
|
||||
|
||||
try {
|
||||
await loadDependencies();
|
||||
|
||||
switch (command) {
|
||||
case 'create': {
|
||||
const name = args[1] || 'untitled';
|
||||
|
||||
// Validate name (alphanumeric only)
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
||||
throw new Error('Invalid name: must be alphanumeric with dashes/underscores only');
|
||||
}
|
||||
|
||||
const dag = new RuDag({ name, storage: null, autoSave: false });
|
||||
await dag.init();
|
||||
|
||||
// Create a simple example DAG
|
||||
const scan = dag.addNode(DagOperator.SCAN, 10.0);
|
||||
const filter = dag.addNode(DagOperator.FILTER, 2.0);
|
||||
const project = dag.addNode(DagOperator.PROJECT, 1.0);
|
||||
|
||||
dag.addEdge(scan, filter);
|
||||
dag.addEdge(filter, project);
|
||||
|
||||
if (isJson) {
|
||||
console.log(dag.toJSON());
|
||||
} else {
|
||||
const bytes = dag.toBytes();
|
||||
process.stdout.write(Buffer.from(bytes));
|
||||
}
|
||||
|
||||
dag.dispose();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'load': {
|
||||
const file = validateFilePath(args[1]);
|
||||
|
||||
if (!fs.existsSync(file)) {
|
||||
throw new Error(`File not found: ${args[1]}`);
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(file);
|
||||
let dag;
|
||||
|
||||
if (file.endsWith('.json')) {
|
||||
dag = await RuDag.fromJSON(data.toString(), { storage: null });
|
||||
} else {
|
||||
dag = await RuDag.fromBytes(new Uint8Array(data), { storage: null });
|
||||
}
|
||||
|
||||
console.log(`Loaded DAG with ${dag.nodeCount} nodes and ${dag.edgeCount} edges`);
|
||||
dag.dispose();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'info': {
|
||||
const file = validateFilePath(args[1]);
|
||||
|
||||
if (!fs.existsSync(file)) {
|
||||
throw new Error(`File not found: ${args[1]}`);
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(file);
|
||||
let dag;
|
||||
|
||||
if (file.endsWith('.json')) {
|
||||
dag = await RuDag.fromJSON(data.toString(), { storage: null });
|
||||
} else {
|
||||
dag = await RuDag.fromBytes(new Uint8Array(data), { storage: null });
|
||||
}
|
||||
|
||||
const critPath = dag.criticalPath();
|
||||
const info = {
|
||||
file: args[1],
|
||||
nodes: dag.nodeCount,
|
||||
edges: dag.edgeCount,
|
||||
criticalPath: critPath,
|
||||
};
|
||||
|
||||
if (isJson) {
|
||||
console.log(JSON.stringify(info, null, 2));
|
||||
} else {
|
||||
console.log(`File: ${info.file}`);
|
||||
console.log(`Nodes: ${info.nodes}`);
|
||||
console.log(`Edges: ${info.edges}`);
|
||||
console.log(`Critical Path: ${info.criticalPath.path.join(' -> ')}`);
|
||||
console.log(`Total Cost: ${info.criticalPath.cost}`);
|
||||
}
|
||||
|
||||
dag.dispose();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'topo': {
|
||||
const file = validateFilePath(args[1]);
|
||||
|
||||
if (!fs.existsSync(file)) {
|
||||
throw new Error(`File not found: ${args[1]}`);
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(file);
|
||||
let dag;
|
||||
|
||||
if (file.endsWith('.json')) {
|
||||
dag = await RuDag.fromJSON(data.toString(), { storage: null });
|
||||
} else {
|
||||
dag = await RuDag.fromBytes(new Uint8Array(data), { storage: null });
|
||||
}
|
||||
|
||||
const topo = dag.topoSort();
|
||||
|
||||
if (isJson) {
|
||||
console.log(JSON.stringify(topo));
|
||||
} else {
|
||||
console.log('Topological order:', topo.join(' -> '));
|
||||
}
|
||||
|
||||
dag.dispose();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'critical': {
|
||||
const file = validateFilePath(args[1]);
|
||||
|
||||
if (!fs.existsSync(file)) {
|
||||
throw new Error(`File not found: ${args[1]}`);
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(file);
|
||||
let dag;
|
||||
|
||||
if (file.endsWith('.json')) {
|
||||
dag = await RuDag.fromJSON(data.toString(), { storage: null });
|
||||
} else {
|
||||
dag = await RuDag.fromBytes(new Uint8Array(data), { storage: null });
|
||||
}
|
||||
|
||||
const result = dag.criticalPath();
|
||||
|
||||
if (isJson) {
|
||||
console.log(JSON.stringify(result));
|
||||
} else {
|
||||
console.log('Critical Path:', result.path.join(' -> '));
|
||||
console.log('Total Cost:', result.cost);
|
||||
}
|
||||
|
||||
dag.dispose();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'attention': {
|
||||
const file = validateFilePath(args[1]);
|
||||
const type = args[2] || 'critical';
|
||||
|
||||
if (!fs.existsSync(file)) {
|
||||
throw new Error(`File not found: ${args[1]}`);
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(file);
|
||||
let dag;
|
||||
|
||||
if (file.endsWith('.json')) {
|
||||
dag = await RuDag.fromJSON(data.toString(), { storage: null });
|
||||
} else {
|
||||
dag = await RuDag.fromBytes(new Uint8Array(data), { storage: null });
|
||||
}
|
||||
|
||||
let mechanism;
|
||||
switch (type) {
|
||||
case 'topo':
|
||||
case 'topological':
|
||||
mechanism = AttentionMechanism.TOPOLOGICAL;
|
||||
break;
|
||||
case 'critical':
|
||||
case 'critical_path':
|
||||
mechanism = AttentionMechanism.CRITICAL_PATH;
|
||||
break;
|
||||
case 'uniform':
|
||||
mechanism = AttentionMechanism.UNIFORM;
|
||||
break;
|
||||
default:
|
||||
dag.dispose();
|
||||
throw new Error(`Unknown attention type: ${type}. Use: topo, critical, or uniform`);
|
||||
}
|
||||
|
||||
const scores = dag.attention(mechanism);
|
||||
|
||||
if (isJson) {
|
||||
console.log(JSON.stringify({ type, scores }));
|
||||
} else {
|
||||
console.log(`Attention type: ${type}`);
|
||||
scores.forEach((score, i) => {
|
||||
console.log(` Node ${i}: ${score.toFixed(4)}`);
|
||||
});
|
||||
}
|
||||
|
||||
dag.dispose();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'convert': {
|
||||
const inFile = validateFilePath(args[1]);
|
||||
const outFile = validateOutputPath(args[2]);
|
||||
|
||||
if (!fs.existsSync(inFile)) {
|
||||
throw new Error(`Input file not found: ${args[1]}`);
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(inFile);
|
||||
let dag;
|
||||
|
||||
if (inFile.endsWith('.json')) {
|
||||
dag = await RuDag.fromJSON(data.toString(), { storage: null });
|
||||
} else {
|
||||
dag = await RuDag.fromBytes(new Uint8Array(data), { storage: null });
|
||||
}
|
||||
|
||||
if (outFile.endsWith('.json')) {
|
||||
fs.writeFileSync(outFile, dag.toJSON());
|
||||
} else {
|
||||
fs.writeFileSync(outFile, Buffer.from(dag.toBytes()));
|
||||
}
|
||||
|
||||
console.log(`Converted ${args[1]} -> ${args[2]}`);
|
||||
dag.dispose();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.error(`Unknown command: ${command}`);
|
||||
console.log('Run "rudag help" for usage information');
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
if (verbose) {
|
||||
console.error(error.stack);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user