Pārlūkot izejas kodu

qfu: initial commit with version 1.0.0.0

- command 'clone' add
Dalibor Votruba 1 gadu atpakaļ
vecāks
revīzija
ed63eb686c

+ 81 - 0
qfu/Commands/Aspects/FilesAspect.cs

@@ -0,0 +1,81 @@
+using Quadarax.Foundation.Core.QConsole.Argument;
+using Quadarax.Foundation.Core.QConsole.Command.Base;
+using System.IO.Abstractions;
+using System.Text;
+
+namespace qdr.app.tools.qfu.Commands.Aspects
+{
+    internal class FilesAspect
+    {
+        #region *** Constants ***
+        private const string ARG_FILES_SEP = "|";
+        private const string ARG_FILES_NAME = "files";
+        private const string ARG_FILES_HINT = "files";
+        private const string ARG_FILES_DESCR = "One or more files separated by character pipe (" + ARG_FILES_SEP + ")";
+        private const int ARG_SRC_ORD = 1;
+
+        public const string ARG_PATHENS_NAME = "pe";
+        private const string ARG_PATHENS_HINT = "path_ensure";
+        private const string ARG_PATHENS_DESCR = "Flag if set, then path of the files will be ensured (created if not exists). Otherwise fails.";
+        #endregion
+
+        #region *** Public Operations ***
+        public static IEnumerable<AbstractArgument> SetupArguments()
+        {
+            return
+            [
+                new OrdinalArgument(ARG_SRC_ORD, false, ARG_FILES_NAME, ARG_FILES_DESCR, ARG_FILES_HINT,Quadarax.Foundation.Core.QConsole.Value.TypeValuesEnum.String, string.Empty, true),
+                new FlagArgument(ARG_PATHENS_NAME, ARG_PATHENS_DESCR, ARG_PATHENS_HINT,false)
+            ];
+        }
+
+        public static bool EnsureFilePath(IFileSystem fs, string filePath)
+        {
+            if (string.IsNullOrEmpty(filePath))
+                throw new ArgumentException("File path cannot be null or empty.", nameof(filePath));
+            var directory = Path.GetDirectoryName(filePath);
+            if (string.IsNullOrEmpty(directory) || fs.Directory.Exists(directory))
+                return true;
+            fs.Directory.CreateDirectory(directory);
+            return false;
+        }
+
+        public static string[] GetFiles(AbstractCommand context, IFileSystem fs, bool checkValidity = false)
+        {
+
+            if (context == null)
+                throw new ArgumentNullException(nameof(context), "Command context cannot be null.");
+            var filesArgument = context.Arguments.First(x => string.Equals(x.Code, ARG_FILES_NAME));
+            if (filesArgument.Value.Get() == null)
+                throw new ArgumentException($"Argument '{ARG_FILES_NAME}' is not set.", ARG_FILES_NAME);
+            var files = ParseFiles(filesArgument.Value.Get()?.ToString()!);
+
+
+            if (checkValidity)
+            {
+                var sb = new StringBuilder();
+                foreach (var file in files)
+                {
+                    if (string.IsNullOrEmpty(file))
+                        continue;
+                    if (!fs.File.Exists(file) && !fs.Directory.Exists(file))
+                        sb.AppendLine(file);
+                }
+                if (sb.Length > 0)
+                    throw new FileNotFoundException($"The following files do not exist:\n{sb.ToString()}");
+            }
+            return files;
+        }
+
+        public static string[] ParseFiles(string filesArgument)
+        {
+            if (string.IsNullOrEmpty(filesArgument))
+                throw new ArgumentException($"Argument '{ARG_FILES_NAME}' is not defined or empty.", ARG_FILES_NAME);
+            return filesArgument.Split(new[] { ARG_FILES_SEP }, StringSplitOptions.RemoveEmptyEntries)
+                                 .Select(f => f.Trim())
+                                 .Where(f => !string.IsNullOrEmpty(f))
+                                 .ToArray();
+        }
+        #endregion
+    }
+}

+ 24 - 0
qfu/Commands/Aspects/ForceAspect.cs

@@ -0,0 +1,24 @@
+using Quadarax.Foundation.Core.QConsole.Argument;
+
+namespace qdr.app.tools.qfu.Commands.Aspects
+{
+    internal class ForceAspect
+    {
+        #region *** Constants ***
+        public const string ARG_FORCE_NAME = "force";
+        private const string ARG_FORCE_HINT = "force";
+        private const string ARG_FORCE_DESCR = "Flag if set then ovewrite destination file therefore exists.";
+        #endregion
+
+
+        #region *** Public Operations ***
+        public static IEnumerable<AbstractArgument> SetupArguments()
+        {
+            return
+            [
+                new FlagArgument( ARG_FORCE_NAME, ARG_FORCE_DESCR, ARG_FORCE_HINT,false)
+            ];
+        }
+        #endregion
+    }
+}

+ 60 - 0
qfu/Commands/Aspects/ListAspect.cs

@@ -0,0 +1,60 @@
+using Quadarax.Foundation.Core.QConsole.Argument;
+using Quadarax.Foundation.Core.QConsole.Command.Base;
+using System;
+using System.Collections.Generic;
+using System.IO.Abstractions;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace qdr.app.tools.qfu.Commands.Aspects
+{
+    internal class ListAspect
+    {
+        #region *** Constants ***
+        private const string ARG_LIST_NAME = "list";
+        private const string ARG_LIST_HINT = "list";
+        private const string ARG_LIST_DESCR = "Flag if defined, then first argument of target file represents text file with new line defined full file paths";
+        #endregion
+
+        #region *** Public Operations ***
+        public static IEnumerable<AbstractArgument> SetupArguments()
+        {
+            return
+            [
+                new FlagArgument(ARG_LIST_NAME, ARG_LIST_DESCR, ARG_LIST_HINT, false)
+            ];
+        }
+
+        public static string[] GetFiles(AbstractCommand context, IFileSystem fs,IEnumerable<string> defaultResult, bool checkValidity = false)
+        {
+            if (context == null)
+                throw new ArgumentNullException(nameof(context), "Command context cannot be null.");
+            var filesArgument = context.Arguments.First(x => string.Equals(x.Code, ARG_LIST_NAME));
+            if (filesArgument.Value.Get() == null)
+                throw new ArgumentException($"Argument '{ARG_LIST_NAME}' is not set.", ARG_LIST_NAME);
+            if (!(bool)filesArgument.Value.Get()!)
+                return defaultResult.ToArray();
+
+            var listFileName = defaultResult.First();
+            if (!fs.File.Exists(listFileName))
+                throw new FileNotFoundException($"List file '{listFileName}' does not exist.");
+            var files = fs.File.ReadAllLines(listFileName).Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();
+            if (checkValidity)
+            {
+                var sb = new StringBuilder();
+                foreach (var file in files)
+                {
+                    if (string.IsNullOrEmpty(file))
+                        continue;
+                    if (!fs.File.Exists(file) && !fs.Directory.Exists(file))
+                        sb.AppendLine(file);
+                }
+                if (sb.Length > 0)
+                    throw new FileNotFoundException($"The following files do not exist:\n{sb.ToString()}");
+            }
+            return files;
+        }
+        #endregion
+    }
+}

+ 30 - 0
qfu/Commands/Aspects/SourceAspect.cs

@@ -0,0 +1,30 @@
+using Quadarax.Foundation.Core.QConsole.Argument;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace qdr.app.tools.qfu.Commands.Aspects
+{
+    internal class SourceAspect
+    {
+        #region *** Constants ***
+        public const string ARG_SRC_NAME = "source";
+        private const string ARG_SRC_HINT = "source";
+        private const string ARG_SRC_DESCR = "Source file or directory name to operate with.";
+        private const int ARG_SRC_ORD = 0;
+        #endregion
+
+
+        #region *** Public Operations ***
+        public static IEnumerable<AbstractArgument> SetupArguments()
+        {
+            return
+            [
+                new OrdinalArgument(ARG_SRC_ORD,false, ARG_SRC_NAME, ARG_SRC_DESCR, ARG_SRC_HINT, Quadarax.Foundation.Core.QConsole.Value.TypeValuesEnum.String, string.Empty, true)
+            ];
+        }
+        #endregion
+    }
+}

+ 54 - 0
qfu/Commands/BaseCmd.cs

@@ -0,0 +1,54 @@
+using qdr.app.tools.qfu.Commands.Aspects;
+using Quadarax.Foundation.Core.QConsole;
+using Quadarax.Foundation.Core.QConsole.Argument;
+using Quadarax.Foundation.Core.QConsole.Command.Base;
+using System.IO.Abstractions;
+
+namespace qdr.app.tools.qfu.Commands
+{
+    abstract class BaseCmd : AbstractCommand
+    {
+        #region *** Properties ***
+        protected string Source { get; private set; } = string.Empty;
+        protected IFileSystem FileSystem => GetContext<QfuCommandContext>().FileSystem;
+        #endregion
+
+        #region *** Constructors ***
+        protected BaseCmd(Engine engine) : base(engine)
+        {
+        }
+        #endregion
+
+        #region *** Protected Operations ***
+        protected string ArgumentAsFileName(string argumentName)
+        {
+            var fileName = GetArgumentValueOrDefault<string>(argumentName);
+            if (string.IsNullOrEmpty(fileName))
+                throw new ArgumentException($"Argument '{argumentName}' is not defined or empty.", argumentName);
+            return fileName;
+        }
+        protected void CheckSourceFileExists()
+        {
+            if (!FileSystem.File.Exists(Source))
+                throw new ArgumentException($"Source file '{Source}' does not exist.", SourceAspect.ARG_SRC_NAME);
+        }
+        #endregion
+
+        #region *** Overrides ***
+        protected override IEnumerable<AbstractArgument> OnSetupArguments()
+        {
+            var list = base.OnSetupArguments().ToList();
+           list.AddRange(SourceAspect.SetupArguments());
+           return list;
+        }
+
+        protected override void OnValidateArguments()
+        {
+            base.OnValidateArguments();
+            Source = ArgumentAsFileName(SourceAspect.ARG_SRC_NAME);
+        }
+        #endregion
+
+
+    }
+}

+ 96 - 0
qfu/Commands/CloneCmd.cs

@@ -0,0 +1,96 @@
+using qdr.app.tools.qfu.Commands.Aspects;
+using Quadarax.Foundation.Core.QConsole;
+using Quadarax.Foundation.Core.QConsole.Argument;
+using Quadarax.Foundation.Core.QConsole.Attributes;
+using Quadarax.Foundation.Core.Value;
+
+namespace qdr.app.tools.qfu.Commands
+{
+    [CommandDefinition]
+
+    internal class CloneCmd : BaseCmd
+    {
+        #region *** Constants ***
+        private const string CMD_NAME = "clone";
+        private const string CMD_DESCR = "Clone source file to files specified.";
+        #endregion
+
+        #region *** Properties ***
+        public override string Name => CMD_NAME;
+        public override string Description => CMD_DESCR;
+
+        protected string[] TargetFiles { get; private set; } = Array.Empty<string>();
+        protected bool IsEnsurePath { get; private set; } = false;
+        protected bool IsForce { get; private set; } = false;
+        #endregion
+
+        #region *** Constructors ***
+        public CloneCmd(Engine engine) : base(engine)
+        {
+        }
+        #endregion
+
+        #region *** Overrides ***
+        #region **** EXECUTE ****
+        protected override Result OnExecute()
+        {
+            WriteCaption($"Will be clonig file '{Source}' to {TargetFiles.Length} files {(IsForce ? "(with force)": string.Empty)}... ");
+            var cnt = 0;
+            var succ = 0;
+            var bSucc = true;
+            foreach (var file in TargetFiles)
+            {
+                cnt++;
+                if (FileSystem.File.Exists(file) && !IsEnsurePath)
+                {
+                    if (!IsForce)
+                    {
+                        WriteWarning($"[{cnt}] File '{file}' already exists. Skipping clone operation.");
+                        continue;
+                    }
+                }
+                try
+                {
+                    FileSystem.File.Copy(Source, file, true);
+                    if (FileSystem.File.Exists(file))
+                        succ++;
+                    else
+                        WriteWarning($"[{cnt}] File '{file}' does not exist after copy operation.");
+                    WriteInfo($"[{cnt}] Cloned '{Source}' to '{file}' successfully.");
+                }
+                catch (Exception ex)
+                {
+                    WriteError($"[{cnt}] Failed to clone '{Source}' to '{file}': {ex.Message}");
+                    bSucc = false;
+                }
+            }
+
+            WriteCaption($"Succesfully cloned: {succ} of {cnt} files.");
+            bSucc = succ == cnt;
+            return bSucc ? new Result() : new Result(new Exception($"Failed to clone {TargetFiles.Length - succ} files from '{Source}'."));
+        }
+        #endregion
+        #region **** Arguments ****
+        protected override void OnValidateArguments()
+        {
+            base.OnValidateArguments();
+            CheckSourceFileExists();
+            TargetFiles = ListAspect.GetFiles(this, FileSystem, FilesAspect.GetFiles(this, FileSystem, false));
+            IsEnsurePath = GetArgumentValueOrDefault<bool>(FilesAspect.ARG_PATHENS_NAME, false);
+            IsForce = GetArgumentValueOrDefault<bool>(ForceAspect.ARG_FORCE_NAME, false);
+        }
+
+        protected override IEnumerable<AbstractArgument> OnSetupArguments()
+        {
+            var list = base.OnSetupArguments().ToList();
+            list.AddRange(ListAspect.SetupArguments());
+            list.AddRange(FilesAspect.SetupArguments());
+            list.AddRange(ForceAspect.SetupArguments());
+            return list;
+        }
+
+        #endregion
+        #endregion
+    }
+}
+

+ 25 - 0
qfu/Program.cs

@@ -0,0 +1,25 @@
+// See https://aka.ms/new-console-template for more information
+using qdr.app.tools.qfu;
+using Quadarax.Foundation.Core.QConsole;
+using Quadarax.Foundation.Core.QConsole.Configuration;
+using Quadarax.Foundation.Core.Reflection.Extensions;
+using System.IO.Abstractions;
+using System.Reflection;
+
+var consoleVersion = Assembly.GetExecutingAssembly()?.GetName()?.Version;
+if (consoleVersion != null)
+{
+    var config = new StartupConfiguration("Quadarax Filesystem Tools", consoleVersion)
+    {
+        ConsoleCopyright = Assembly.GetExecutingAssembly().GetCopyright(),
+        AllowInteractive = false,
+        WaitOnKeyInNonInteractiveMode = false,
+        CommandDefinitionAssembly = new Assembly[] { Assembly.GetEntryAssembly()! }
+    };
+    config.DisableDefaultCommands();
+    config.ShowStatusOnEveryCommand = false;
+    config.IsConsoleDebug = false;
+
+    var console = new Engine(config, new QfuEngineContext(new FileSystem()), args);
+    console.Start();
+}

+ 17 - 0
qfu/Properties/PublishProfiles/FolderProfile.pubxml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
+<Project>
+  <PropertyGroup>
+    <Configuration>Release</Configuration>
+    <Platform>Any CPU</Platform>
+    <PublishDir>D:\Deploy</PublishDir>
+    <PublishProtocol>FileSystem</PublishProtocol>
+    <_TargetId>Folder</_TargetId>
+    <TargetFramework>net9.0</TargetFramework>
+    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
+    <SelfContained>true</SelfContained>
+    <PublishSingleFile>true</PublishSingleFile>
+    <PublishReadyToRun>true</PublishReadyToRun>
+    <PublishTrimmed>false</PublishTrimmed>
+  </PropertyGroup>
+</Project>

+ 15 - 0
qfu/Properties/launchSettings.json

@@ -0,0 +1,15 @@
+{
+  "profiles": {
+    "qfu": {
+      "commandName": "Project"
+    },
+    "qfu -help": {
+      "commandName": "Project",
+      "commandLineArgs": "help"
+    },
+    "qfu clone": {
+      "commandName": "Project",
+      "commandLineArgs": "clone test.dat \"test01.dat\"|\"test02.dat\"|\"test03.dat\""
+    }
+  }
+}

+ 26 - 0
qfu/QfuCommandContext.cs

@@ -0,0 +1,26 @@
+using Quadarax.Foundation.Core.Object;
+using Quadarax.Foundation.Core.QConsole.Context;
+using System.IO.Abstractions;
+
+namespace qdr.app.tools.qfu
+{
+    internal class QfuCommandContext : DisposableObject, ICommandContext
+    {
+
+        private IFileSystem? _fileSystem;
+
+        public IFileSystem FileSystem => _fileSystem ?? new FileSystem();
+
+        public string Status => "READY";
+
+        public QfuCommandContext(IFileSystem fileSystem)
+        {
+            _fileSystem = fileSystem;
+        }
+
+        protected override void OnDisposing()
+        {
+            _fileSystem = null;
+        }
+    }
+}

+ 31 - 0
qfu/QfuEngineContext.cs

@@ -0,0 +1,31 @@
+using Quadarax.Foundation.Core.Object;
+using Quadarax.Foundation.Core.QConsole.Context;
+using System.IO.Abstractions;
+
+namespace qdr.app.tools.qfu
+{
+    internal class QfuEngineContext : DisposableObject, IEngineContext
+    {
+
+        private IFileSystem? _fileSystem;
+
+        public string Status => throw new NotImplementedException();
+
+
+        public QfuEngineContext(IFileSystem fileSystem)
+        {
+            _fileSystem = fileSystem;
+        }
+
+
+        public ICommandContext CreateCommandContext()
+        {
+            return new QfuCommandContext(_fileSystem ?? new FileSystem());
+        }
+
+        protected override void OnDisposing()
+        {
+            _fileSystem = null;
+        }
+    }
+}

+ 27 - 0
qfu/directory_structure.txt

@@ -0,0 +1,27 @@
+Project Directory Structure 
+Generated on: 12.06.2025 18:46:19,06 
+Root: d:\Projects\quadarax\qdr.app\qdr.app.tools\qfu 
+ 
+directory_structure.txt 
+gen_dirstruct.cmd 
+gen_srcout.cmd 
+Program.cs 
+qfu.csproj 
+qfu.ico 
+qfu.sln 
+QfuCommandContext.cs 
+QfuEngineContext.cs 
+readme.md 
+releasenotes.md 
+Commands/ 
+  BaseCmd.cs 
+  CloneCmd.cs 
+  Aspects/ 
+    FilesAspect.cs 
+    ForceAspect.cs 
+    ListAspect.cs 
+    SourceAspect.cs 
+Properties/ 
+  launchSettings.json 
+  PublishProfiles/ 
+    FolderProfile.pubxml 

+ 73 - 0
qfu/gen_dirstruct.cmd

@@ -0,0 +1,73 @@
+@echo off
+setlocal enabledelayedexpansion
+
+rem Generate directory structure file for Visual Studio extension project
+rem Excludes: .vs, bin, obj directories and *.user files
+
+set OUTPUT_FILE=directory_structure.txt
+set CURRENT_DIR=%CD%
+
+echo Generating directory structure...
+echo Project Directory Structure > "%OUTPUT_FILE%"
+echo Generated on: %DATE% %TIME% >> "%OUTPUT_FILE%"
+echo Root: %CURRENT_DIR% >> "%OUTPUT_FILE%"
+echo. >> "%OUTPUT_FILE%"
+
+rem Function to process directory recursively
+call :ProcessDirectory "." ""
+
+echo.
+echo Directory structure generated successfully in: %OUTPUT_FILE%
+pause
+goto :eof
+
+:ProcessDirectory
+set "dir_path=%~1"
+set "indent=%~2"
+
+rem Get directory name for display
+for %%F in ("%dir_path%") do set "dir_name=%%~nxF"
+
+rem Skip excluded directories
+if /i "%dir_name%"==".vs" goto :eof
+if /i "%dir_name%"=="bin" goto :eof
+if /i "%dir_name%"=="obj" goto :eof
+if /i "%dir_name%"=="packages" goto :eof
+if /i "%dir_name%"==".git" goto :eof
+if /i "%dir_name%"=="node_modules" goto :eof
+
+rem Print current directory (except root)
+if not "%dir_path%"=="." (
+    echo %indent%%dir_name%/ >> "%OUTPUT_FILE%"
+    set "new_indent=%indent%  "
+) else (
+    set "new_indent="
+)
+
+rem Process files in current directory
+for %%F in ("%dir_path%\*") do (
+    set "file_name=%%~nxF"
+    set "file_ext=%%~xF"
+    
+    rem Skip user-specific and temporary files
+    if /i not "!file_ext!"==".user" (
+        if /i not "!file_ext!"==".suo" (
+            if /i not "!file_ext!"==".cache" (
+                if /i not "!file_name!"==".gitignore" (
+                    if /i not "!file_name!"=="Thumbs.db" (
+                        if /i not "!file_name!"=="Desktop.ini" (
+                            echo %new_indent%!file_name! >> "%OUTPUT_FILE%"
+                        )
+                    )
+                )
+            )
+        )
+    )
+)
+
+rem Process subdirectories
+for /d %%D in ("%dir_path%\*") do (
+    call :ProcessDirectory "%%D" "%new_indent%"
+)
+
+goto :eof

+ 172 - 0
qfu/gen_srcout.cmd

@@ -0,0 +1,172 @@
+@echo off
+setlocal enabledelayedexpansion
+
+echo ====================================
+echo Visual Studio Solution Files Copier
+echo ====================================
+echo.
+
+REM Get the current directory
+set "SOURCE_DIR=%CD%"
+set "TARGET_DIR=%SOURCE_DIR%\.srcout"
+
+echo Source Directory: %SOURCE_DIR%
+echo Target Directory: %TARGET_DIR%
+echo.
+
+REM Clear and create target directory
+if exist "%TARGET_DIR%" (
+    echo Clearing existing .srcout directory...
+    rmdir /s /q "%TARGET_DIR%" 2>nul
+    if exist "%TARGET_DIR%" (
+        echo Warning: Could not completely clear .srcout directory
+        timeout /t 2 >nul
+    )
+)
+
+echo Creating .srcout directory...
+mkdir "%TARGET_DIR%" 2>nul
+if not exist "%TARGET_DIR%" (
+    echo Error: Could not create .srcout directory
+    pause
+    exit /b 1
+)
+
+echo.
+echo Copying files to .srcout root directory...
+
+REM Initialize counters
+set "COPIED_COUNT=0"
+set "SKIPPED_COUNT=0"
+
+REM Function to copy files recursively, flattening directory structure
+call :CopyFilesFlat "%SOURCE_DIR%"
+
+echo.
+echo Copy operation completed!
+echo Files copied: %COPIED_COUNT%
+echo Files skipped: %SKIPPED_COUNT%
+
+goto :ContinueScript
+
+:CopyFilesFlat
+setlocal
+set "CURRENT_DIR=%~1"
+
+REM Process files in current directory
+for %%f in ("%CURRENT_DIR%\*") do (
+    if not "%%~nxf"=="" (
+        call :CopyFile "%%f"
+    )
+)
+
+REM Process subdirectories (excluding blacklisted ones)
+for /d %%d in ("%CURRENT_DIR%\*") do (
+    set "DIR_NAME=%%~nxd"
+    set "SKIP_DIR=0"
+    
+    REM Check if directory should be excluded
+    if /i "!DIR_NAME!"==".vs" set "SKIP_DIR=1"
+    if /i "!DIR_NAME!"=="bin" set "SKIP_DIR=1"
+    if /i "!DIR_NAME!"=="obj" set "SKIP_DIR=1"
+    if /i "!DIR_NAME!"=="packages" set "SKIP_DIR=1"
+    if /i "!DIR_NAME!"=="TestResults" set "SKIP_DIR=1"
+    if /i "!DIR_NAME!"==".git" set "SKIP_DIR=1"
+    if /i "!DIR_NAME!"==".srcout" set "SKIP_DIR=1"
+    if /i "!DIR_NAME!"=="node_modules" set "SKIP_DIR=1"
+    if /i "!DIR_NAME!"==".nuget" set "SKIP_DIR=1"
+    
+    if "!SKIP_DIR!"=="0" (
+        call :CopyFilesFlat "%%d"
+    )
+)
+endlocal
+goto :eof
+
+:CopyFile
+setlocal
+set "SOURCE_FILE=%~1"
+set "FILE_NAME=%~nx1"
+set "FILE_EXT=%~x1"
+set "SKIP_FILE=0"
+
+REM Check if file should be excluded
+if /i "%FILE_EXT%"==".user" set "SKIP_FILE=1"
+if /i "%FILE_EXT%"==".suo" set "SKIP_FILE=1"
+if /i "%FILE_EXT%"==".cache" set "SKIP_FILE=1"
+if /i "%FILE_EXT%"==".tmp" set "SKIP_FILE=1"
+if /i "%FILE_EXT%"==".temp" set "SKIP_FILE=1"
+if /i "%FILE_EXT%"==".log" set "SKIP_FILE=1"
+if /i "%FILE_EXT%"==".pdb" set "SKIP_FILE=1"
+if /i "%FILE_EXT%"==".exe" set "SKIP_FILE=1"
+if /i "%FILE_EXT%"==".dll" set "SKIP_FILE=1"
+if /i "%FILE_EXT%"==".png" set "SKIP_FILE=1"
+if /i "%FILE_EXT%"==".jpg" set "SKIP_FILE=1"
+if /i "%FILE_EXT%"==".gif" set "SKIP_FILE=1"
+if /i "%FILE_EXT%"==".ico" set "SKIP_FILE=1"
+if /i "%FILE_EXT%"==".snk" set "SKIP_FILE=1"
+if /i "%FILE_EXT%"==".vspscc" set "SKIP_FILE=1"
+if /i "%FILE_EXT%"==".vssscc" set "SKIP_FILE=1"
+if /i "%FILE_EXT%"==".scc" set "SKIP_FILE=1"
+if /i "%FILE_NAME%"==".DS_Store" set "SKIP_FILE=1"
+if /i "%FILE_NAME%"=="Thumbs.db" set "SKIP_FILE=1"
+if /i "%FILE_NAME%"=="desktop.ini" set "SKIP_FILE=1"
+
+if "%SKIP_FILE%"=="0" (
+    REM Handle duplicate filenames by adding a counter
+    set "TARGET_FILE=%TARGET_DIR%\%FILE_NAME%"
+    set "COUNTER=1"
+    
+    :CheckDuplicate
+    if exist "!TARGET_FILE!" (
+        set "BASE_NAME=%~n1"
+        set "TARGET_FILE=%TARGET_DIR%\!BASE_NAME!_!COUNTER!%FILE_EXT%"
+        set /a "COUNTER+=1"
+        goto :CheckDuplicate
+    )
+    
+    copy "%SOURCE_FILE%" "!TARGET_FILE!" >nul 2>&1
+    if !ERRORLEVEL! EQU 0 (
+        set /a "COPIED_COUNT+=1"
+        echo Copied: %FILE_NAME%
+    ) else (
+        echo Failed to copy: %FILE_NAME%
+    )
+) else (
+    set /a "SKIPPED_COUNT+=1"
+)
+endlocal
+goto :eof
+
+:ContinueScript
+
+REM Count files copied
+echo.
+echo Counting files in .srcout directory...
+set "FINAL_COUNT=0"
+for /f %%i in ('dir "%TARGET_DIR%" /b /a-d 2^>nul ^| find /c /v ""') do set "FINAL_COUNT=%%i"
+
+echo.
+echo ====================================
+echo Copy Summary:
+echo ====================================
+echo Files copied: %FINAL_COUNT%
+echo Files processed: %COPIED_COUNT%
+echo Files skipped: %SKIPPED_COUNT%
+echo Source: %SOURCE_DIR%
+echo Target: %TARGET_DIR%
+echo.
+echo All files are copied to .srcout root directory (flattened structure)
+echo Excluded directories: .vs, bin, obj, packages, TestResults, .git, .srcout, node_modules, .nuget
+echo Excluded files: *.user, *.suo, *.cache, *.tmp, *.temp, *.log, *.pdb, *.exe, *.dll, and others
+echo ====================================
+
+REM Optional: Open target directory in Explorer
+set /p "OPEN_FOLDER=Open .srcout folder in Explorer? (y/n): "
+if /i "%OPEN_FOLDER%"=="y" (
+    explorer "%TARGET_DIR%"
+)
+
+echo.
+echo Script completed. Press any key to exit...
+pause >nul

+ 39 - 0
qfu/qfu.csproj

@@ -0,0 +1,39 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net9.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <Title>Quadarax Filesystem Utility</Title>
+    <Authors>Quadarax</Authors>
+    <Company>Quadarax</Company>
+    <Description>Quadarax Filesystem Utility to privide operations and manipulations with directory and files</Description>
+    <Copyright>Quadarax © 2025</Copyright>
+    <PackageProjectUrl>https://quadarax.com/qfu</PackageProjectUrl>
+    <PackageIcon>qfu.ico</PackageIcon>
+    <PackageTags>FILE, UTILITY</PackageTags>
+    <RootNamespace>qdr.app.tools.qfu</RootNamespace>
+    <SignAssembly>True</SignAssembly>
+    <AssemblyOriginatorKeyFile>D:\Projects\quadarax\qdr.app\qdr.app.tools\bo_strong_key_pair.snk</AssemblyOriginatorKeyFile>
+    <PackageReadmeFile>readme.md</PackageReadmeFile>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="qdr.fnd.core.qconsole" Version="0.0.7-alpha" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <None Update="qfu.ico">
+      <Pack>True</Pack>
+      <PackagePath>\</PackagePath>
+    </None>
+    <None Update="readme.md">
+      <Pack>True</Pack>
+      <PackagePath>\</PackagePath>
+    </None>
+  </ItemGroup>
+	<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(Configuration)' == 'Debug'">
+		<Exec Command="if not exist $(OutDir)test.dat fsutil file createnew $(OutDir)test.dat 100048576" />
+	</Target>
+</Project>

BIN
qfu/qfu.ico


+ 25 - 0
qfu/qfu.sln

@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.14.36127.28 d17.14
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "qfu", "qfu.csproj", "{AB1CC122-C154-4492-B63B-987E75A0FB33}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{AB1CC122-C154-4492-B63B-987E75A0FB33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{AB1CC122-C154-4492-B63B-987E75A0FB33}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{AB1CC122-C154-4492-B63B-987E75A0FB33}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{AB1CC122-C154-4492-B63B-987E75A0FB33}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {074EF9EF-09A6-4C83-AB0D-4222CE0797B0}
+	EndGlobalSection
+EndGlobal

+ 182 - 0
qfu/readme.md

@@ -0,0 +1,182 @@
+# QFU - Quadarax Filesystem Utility
+
+A command-line utility for filesystem operations and file manipulations built on .NET 9.0.
+
+## Overview
+
+QFU is a powerful filesystem utility that provides efficient file operations through a modular command-based architecture. The tool is designed for developers and system administrators who need reliable file manipulation capabilities with robust error handling and flexible input options.
+
+### Key Features
+
+- **File Cloning**: Clone source files to multiple destinations with various options
+- **Flexible Input Methods**: Support for direct file specification or file list input
+- **Path Management**: Automatic directory creation when needed
+- **Force Operations**: Override existing files when necessary
+- **Robust Error Handling**: Comprehensive validation and error reporting
+- **Modular Architecture**: Extensible command and aspect-based design
+
+## Architecture
+
+The application follows a clean modular architecture:
+
+- **Commands**: Core operations (currently `clone`)
+- **Aspects**: Reusable functionality components (Source, Files, List, Force)
+- **Base Classes**: Shared functionality and abstractions
+- **Dependency Injection**: Uses `System.IO.Abstractions` for testable file operations
+
+## Installation
+
+### Prerequisites
+
+- .NET 9.0 Runtime
+- Windows, Linux, or macOS
+
+### Build from Source
+
+```bash
+git clone <repository-url>
+cd qfu
+dotnet build --configuration Release
+dotnet publish --configuration Release --self-contained
+```
+
+## Usage
+
+### Basic Syntax
+
+```
+qfu <command> [arguments] [options]
+```
+
+### Commands
+
+#### Clone Command
+
+Clone a source file to one or more destination files.
+
+**Syntax:**
+```
+qfu clone <source_file> <target_files> [options]
+```
+
+**Arguments:**
+- `source_file`: Path to the source file to clone
+- `target_files`: Pipe-separated list of target file paths (`file1.txt|file2.txt|file3.txt`)
+
+**Options:**
+- `-pe` or `-path_ensure`: Create destination directories if they don't exist
+- `-force`: Overwrite existing destination files
+- `-list`: Treat the first target file as a text file containing a list of destination paths (one per line)
+
+### Examples
+
+#### Basic File Cloning
+```bash
+# Clone a file to multiple destinations
+qfu clone source.txt "backup1.txt|backup2.txt|backup3.txt"
+```
+
+#### Clone with Path Creation
+```bash
+# Clone and create destination directories if needed
+qfu clone template.config "configs/dev.config|configs/staging.config|configs/prod.config" -pe
+```
+
+#### Force Overwrite Existing Files
+```bash
+# Overwrite existing destination files
+qfu clone master.template "project1/config.xml|project2/config.xml" -force -pe
+```
+
+#### Using File Lists
+```bash
+# Create a file list first
+echo "backup/file1.txt" > targets.txt
+echo "backup/file2.txt" >> targets.txt
+echo "backup/file3.txt" >> targets.txt
+
+# Use the list file for cloning
+qfu clone source.txt targets.txt -list -pe
+```
+
+#### Complex Example
+```bash
+# Clone with all options
+qfu clone template.cs "src/Class1.cs|src/Class2.cs|tests/TestClass.cs" -force -pe
+```
+
+### Return Codes
+
+- `0`: Success - all operations completed successfully
+- `Non-zero`: Failure - check console output for specific error details
+
+### Output Format
+
+QFU provides detailed progress information:
+- Operation summary at start
+- Individual file operation status
+- Final success/failure count
+- Detailed error messages for failed operations
+
+Example output:
+```
+Will be cloning file 'source.txt' to 3 files (with force)...
+[1] Cloned 'source.txt' to 'dest1.txt' successfully.
+[2] File 'dest2.txt' already exists. Skipping clone operation.
+[3] Cloned 'source.txt' to 'dest3.txt' successfully.
+Successfully cloned: 2 of 3 files.
+```
+
+## Error Handling
+
+QFU provides comprehensive error handling:
+
+- **Source Validation**: Verifies source file exists before operation
+- **Destination Validation**: Checks for existing files (unless `--force` used)
+- **Path Validation**: Ensures destination paths are valid
+- **Permission Checking**: Reports access denied errors
+- **List File Validation**: Validates list files when using `-list` option
+
+## Development
+
+### Project Structure
+
+```
+qfu/
+├── Commands/
+│   ├── BaseCmd.cs          # Base command functionality
+│   ├── CloneCmd.cs         # Clone command implementation
+│   └── Aspects/
+│       ├── FilesAspect.cs  # File handling logic
+│       ├── ForceAspect.cs  # Force operation logic
+│       ├── ListAspect.cs   # List file processing
+│       └── SourceAspect.cs # Source file validation
+├── QfuCommandContext.cs    # Command execution context
+├── QfuEngineContext.cs     # Engine context
+└── Program.cs              # Application entry point
+```
+
+### Adding New Commands
+
+1. Create a new command class inheriting from `BaseCmd`
+2. Implement required abstract members
+3. Add `[CommandDefinition]` attribute
+4. Define command-specific aspects if needed
+
+### Testing
+
+The project uses `System.IO.Abstractions` for testable file operations. Mock the `IFileSystem` interface for unit testing.
+
+## License
+
+Copyright © 2025 Quadarax
+
+## Support
+
+For issues and feature requests, please visit: https://quadarax.com/qfu
+
+---
+
+**Version**: 1.0.0  
+**Target Framework**: .NET 9.0  
+**Package Tags**: FILE, UTILITY

+ 4 - 0
qfu/releasenotes.md

@@ -0,0 +1,4 @@
+* 1.0.0.
+# 12.6.2025
+- initial version
+- command 'clone' add