feat: implement workspace file tree listing

- Add `list()` method to generate a full workspace file tree
- Introduce `WorkspaceItem` and `WorkspaceList` structs
- Remove `HeropromptSession` to simplify the public API
- Rename Heroscript action to `heropromptworkspace.configure`
- Enable full heroscript encoding/decoding for workspaces
This commit is contained in:
Mahmoud-Emad
2025-08-14 15:45:26 +03:00
parent a58d72615d
commit 2d00d6cf9f
14 changed files with 305 additions and 288 deletions

View File

@@ -2,25 +2,41 @@
import freeflowuniverse.herolib.develop.heroprompt
mut session := heroprompt.new_session()
import freeflowuniverse.herolib.core.playbook
import os
mut workspace1 := session.add_workspace()!
// TODO: Check the name bug
// mut workspace2 := session.add_workspace(name: 'withname')!
mut dir1 := workspace1.add_dir(path: '/Users/mahmoud/code/github/freeflowuniverse/herolib/docker')!
heroscript_config := '
!!heropromptworkspace.configure name:"test workspace" path:"${os.home_dir()}/code/github/freeflowuniverse/herolib"
'
mut plbook := playbook.new(
text: heroscript_config
)!
heroprompt.play(mut plbook)!
mut workspace1 := heroprompt.new_workspace(
path: '${os.home_dir()}/code/github/freeflowuniverse/herolib'
)!
// mut workspace2 := heroprompt.get(
// name: 'test workspace'
// )!
mut dir1 := workspace1.add_dir(path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker')!
dir1.select_file(name: 'docker_ubuntu_install.sh')!
mut dir2 := workspace1.add_dir(
path: '/Users/mahmoud/code/github/freeflowuniverse/herolib/docker/herolib'
path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker/herolib'
)!
dir2.select_file(name: '.gitignore')!
dir2.select_file(name: 'build.sh')!
dir2.select_file(name: 'debug.sh')!
file := dir2.select_file(name: 'debug.sh')!
// println(file.read()!)
mut dir3 := workspace1.add_dir(
path: '/Users/mahmoud/code/github/freeflowuniverse/herolib/docker/postgresql'
path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker/postgresql'
select_all: true
)!

View File

@@ -0,0 +1,15 @@
#!/usr/bin/env -S v -n -w -gc none -cg -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.core.playbook
import os
heroscript_config := '
!!heropromptworkspace.configure name:"test workspace" path:"${os.home_dir()}/code/github/freeflowuniverse/herolib"
'
mut plbook := playbook.new(
text: heroscript_config
)!
heroprompt.play(mut plbook)!

View File

@@ -66,11 +66,13 @@ fn decode_struct[T](_ T, data string) !T {
}
}
} $else $if field.is_array {
if is_struct_array(typ.$(field.name))! {
$if field.is_array {
$if field.typ is $struct {
mut data_fmt := data.replace(action_str, '')
data_fmt = data.replace('define.${obj_name}', 'define')
arr := decode_array(typ.$(field.name), data_fmt)!
typ.$(field.name) = arr
typ.$(field.name) = decode_array[field.elem_type](typ.$(field.name),
data_fmt)!
}
}
}
}

View File

@@ -3,6 +3,15 @@ module heroprompt
import os
import freeflowuniverse.herolib.core.pathlib
@[heap]
pub struct HeropromptDir {
pub mut:
name string
path pathlib.Path
files []&HeropromptFile @[skip; str: skip]
dirs []&HeropromptDir
}
// Parameters for adding a file to a directory
@[params]
pub struct AddFileParams {

View File

@@ -1,5 +1,16 @@
module heroprompt
// import freeflowuniverse.herolib.data.paramsparser
import freeflowuniverse.herolib.core.pathlib
import os
pub struct HeropromptFile {
pub mut:
content string
path pathlib.Path
name string
}
// Utility function to get file extension with special handling for common files
pub fn get_file_extension(filename string) string {
// Handle special cases for common files without extensions
@@ -57,3 +68,9 @@ pub fn get_file_extension(filename string) string {
return parts[parts.len - 1]
}
// Read the file content
pub fn (fl HeropromptFile) read() !string {
content := os.read_file(fl.path.path)!
return content
}

View File

@@ -1,26 +0,0 @@
module heroprompt
import rand
// HeropromptSession manages multiple workspaces for organizing AI prompts
pub struct HeropromptSession {
pub mut:
id string // Unique session identifier
workspaces []&HeropromptWorkspace // List of workspaces in this session
}
// new_session creates a new heroprompt session with a unique ID
pub fn new_session() HeropromptSession {
return HeropromptSession{
id: rand.uuid_v4()
workspaces: []
}
}
// add_workspace creates and adds a new workspace to the session
pub fn (mut self HeropromptSession) add_workspace(args_ NewWorkspaceParams) !&HeropromptWorkspace {
mut wsp := &HeropromptWorkspace{}
wsp = wsp.new(args_)!
self.workspaces << wsp
return wsp
}

View File

@@ -5,19 +5,11 @@ import time
import os
import freeflowuniverse.herolib.core.pathlib
// HeropromptWorkspace represents a workspace containing multiple directories
// and their selected files for AI prompt generation
@[heap]
pub struct HeropromptWorkspace {
pub mut:
name string = 'default' // Workspace name
dirs []&HeropromptDir // List of directories in this workspace
}
@[params]
pub struct NewWorkspaceParams {
pub mut:
struct NewWorkspaceParams {
mut:
name string
path string
}
/// Create a new workspace
@@ -28,10 +20,200 @@ fn (wsp HeropromptWorkspace) new(args_ NewWorkspaceParams) !&HeropromptWorkspace
args.name = generate_random_workspace_name()
}
workspace := get(name: args.name)!
// Validate and set base path
if args.path.len > 0 {
if !os.exists(args.path) {
return error('Workspace path does not exist: ${args.path}')
}
if !os.is_dir(args.path) {
return error('Workspace path is not a directory: ${args.path}')
}
}
mut workspace := &HeropromptWorkspace{
name: args.name
base_path: os.real_path(args.path)
}
return workspace
}
// WorkspaceItem represents a file or directory in the workspace tree
pub struct WorkspaceItem {
pub mut:
name string // Item name (file or directory name)
path string // Full path to the item
is_directory bool // True if this is a directory
is_file bool // True if this is a file
size i64 // File size in bytes (0 for directories)
extension string // File extension (empty for directories)
children []WorkspaceItem // Child items (for directories)
is_expanded bool // Whether directory is expanded in UI
is_selected bool // Whether this item is selected for prompts
depth int // Depth level in the tree (0 = root)
}
// WorkspaceList represents the complete hierarchical listing of a workspace
pub struct WorkspaceList {
pub mut:
root_path string // Root path of the workspace
items []WorkspaceItem // Top-level items in the workspace
total_files int // Total number of files
total_dirs int // Total number of directories
}
// list returns the complete hierarchical structure of the workspace
pub fn (wsp HeropromptWorkspace) list() WorkspaceList {
mut result := WorkspaceList{
root_path: wsp.base_path
}
if wsp.base_path.len == 0 || !os.exists(wsp.base_path) {
return result
}
// Build the complete tree structure (ALL files and directories)
result.items = wsp.build_workspace_tree(wsp.base_path, 0)
wsp.calculate_totals(result.items, mut result)
// Mark selected items
wsp.mark_selected_items(mut result.items)
return result
}
// build_workspace_tree recursively builds the workspace tree structure
fn (wsp HeropromptWorkspace) build_workspace_tree(path string, depth int) []WorkspaceItem {
mut items := []WorkspaceItem{}
entries := os.ls(path) or { return items }
for entry in entries {
full_path := os.join_path(path, entry)
if os.is_dir(full_path) {
mut dir_item := WorkspaceItem{
name: entry
path: full_path
is_directory: true
is_file: false
size: 0
extension: ''
is_expanded: false
is_selected: false
depth: depth
}
// Recursively get children
dir_item.children = wsp.build_workspace_tree(full_path, depth + 1)
items << dir_item
} else if os.is_file(full_path) {
file_info := os.stat(full_path) or { continue }
extension := get_file_extension(entry)
file_item := WorkspaceItem{
name: entry
path: full_path
is_directory: false
is_file: true
size: file_info.size
extension: extension
children: []
is_expanded: false
is_selected: false
depth: depth
}
items << file_item
}
}
// Sort: directories first, then files, both alphabetically
items.sort_with_compare(fn (a &WorkspaceItem, b &WorkspaceItem) int {
if a.is_directory && !b.is_directory {
return -1
}
if !a.is_directory && b.is_directory {
return 1
}
if a.name < b.name {
return -1
}
if a.name > b.name {
return 1
}
return 0
})
return items
}
// calculate_totals counts total files and directories in the workspace
fn (wsp HeropromptWorkspace) calculate_totals(items []WorkspaceItem, mut result WorkspaceList) {
for item in items {
if item.is_directory {
result.total_dirs++
wsp.calculate_totals(item.children, mut result)
} else {
result.total_files++
}
}
}
// mark_selected_items marks which items are currently selected for prompts
fn (wsp HeropromptWorkspace) mark_selected_items(mut items []WorkspaceItem) {
for mut item in items {
// Check if this item is selected by comparing paths
item.is_selected = wsp.is_item_selected(item.path)
// Recursively mark children
if item.is_directory && item.children.len > 0 {
wsp.mark_selected_items(mut item.children)
}
}
}
// is_item_selected checks if a specific path is selected in the workspace
fn (wsp HeropromptWorkspace) is_item_selected(path string) bool {
for dir in wsp.dirs {
// Check if this directory is selected
if dir.path.path == path {
return true
}
// Check if any file in this directory is selected
for file in dir.files {
if file.path.path == path {
return true
}
}
// Recursively check subdirectories
if wsp.is_path_in_selected_dirs(path, dir.dirs) {
return true
}
}
return false
}
// is_path_in_selected_dirs recursively checks subdirectories for selected items
fn (wsp HeropromptWorkspace) is_path_in_selected_dirs(path string, dirs []&HeropromptDir) bool {
for dir in dirs {
if dir.path.path == path {
return true
}
for file in dir.files {
if file.path.path == path {
return true
}
}
if wsp.is_path_in_selected_dirs(path, dir.dirs) {
return true
}
}
return false
}
@[params]
pub struct AddDirParams {
pub mut:

View File

@@ -1,13 +0,0 @@
module heroprompt
// TODO: Implement template-based prompt generation
fn (mut ws HeropromptWorkspace) heroprompt() !string {
// TODO: fill in template based on selection
return ''
}
// TODO: Implement tree visualization utilities
pub fn get_tree() {}
// TODO: Implement prompt formatting utilities
pub fn format_prompt() {}

View File

@@ -2,7 +2,6 @@ module heroprompt
import freeflowuniverse.herolib.core.base
import freeflowuniverse.herolib.core.playbook { PlayBook }
import freeflowuniverse.herolib.ui.console
__global (
heroprompt_global map[string]&HeropromptWorkspace
@@ -35,7 +34,7 @@ pub fn get(args_ ArgsGet) !&HeropromptWorkspace {
if !exists(args)! {
set(obj)!
} else {
heroscript := context.hero_config_get('heroprompt', args.name)!
heroscript := context.hero_config_get('heropromptworkspace', args.name)!
mut obj_ := heroscript_loads(heroscript)!
set_in_mem(obj_)!
}
@@ -52,20 +51,20 @@ pub fn set(o HeropromptWorkspace) ! {
set_in_mem(o)!
mut context := base.context()!
heroscript := heroscript_dumps(o)!
context.hero_config_set('heroprompt', o.name, heroscript)!
context.hero_config_set('heropromptworkspace', o.name, heroscript)!
}
// does the config exists?
pub fn exists(args_ ArgsGet) !bool {
mut context := base.context()!
mut args := args_get(args_)
return context.hero_config_exists('heroprompt', args.name)
return context.hero_config_exists('heropromptworkspace', args.name)
}
pub fn delete(args_ ArgsGet) ! {
mut args := args_get(args_)
mut context := base.context()!
context.hero_config_delete('heroprompt', args.name)!
context.hero_config_delete('heropromptworkspace', args.name)!
if args.name in heroprompt_global {
// del heroprompt_global[args.name]
}
@@ -79,7 +78,7 @@ fn set_in_mem(o HeropromptWorkspace) ! {
}
pub fn play(mut plbook PlayBook) ! {
mut install_actions := plbook.find(filter: 'heroprompt.configure')!
mut install_actions := plbook.find(filter: 'heropromptworkspace.configure')!
if install_actions.len > 0 {
for install_action in install_actions {
heroscript := install_action.heroscript()

View File

@@ -1,51 +1,43 @@
module heroprompt
import freeflowuniverse.herolib.data.paramsparser
// import freeflowuniverse.herolib.data.encoderhero // temporarily commented out
import freeflowuniverse.herolib.core.pathlib
import os
import freeflowuniverse.herolib.data.encoderhero
pub const version = '0.0.0'
const singleton = false
const default = true
// THIS THE THE SOURCE OF THE INFORMATION OF THIS FILE, HERE WE HAVE THE CONFIG OBJECT CONFIGURED AND MODELLED
pub struct HeropromptFile {
// HeropromptWorkspace represents a workspace containing multiple directories
// and their selected files for AI prompt generation
@[heap]
pub struct HeropromptWorkspace {
pub mut:
content string
path pathlib.Path
name string
name string = 'default' // Workspace name
base_path string // Base path of the workspace
dirs []&HeropromptDir // List of directories in this workspace
}
pub struct HeropromptDir {
@[params]
pub struct AddWorkspaceParams {
pub mut:
name string
path pathlib.Path
files []&HeropromptFile
dirs []&HeropromptDir
path string
}
// pub fn (wsp HeropromptWorkspace) to_tag() {
// tag := HeropromptTags.file_map
// // We need to pass it to the template
// }
// add_workspace creates and adds a new workspace
pub fn new_workspace(args_ AddWorkspaceParams) !&HeropromptWorkspace {
mut wsp := &HeropromptWorkspace{}
wsp = wsp.new(name: args_.name, path: args_.path)!
return wsp
}
// // pub fn (dir HeropromptDir) to_tag() {
// // tag := HeropromptTags.file_content
// // // We need to pass it to the template
// // }
// get_workspace gets the saved workspace
pub fn get_workspace(args_ AddWorkspaceParams) !&HeropromptWorkspace {
if args_.name.len == 0 {
return error('Workspace name is required')
}
// pub fn (fil HeropromptFile) to_tag() {
// tag := HeropromptTags.file_content
// // We need to pass it to the template
// }
// pub enum HeropromptTags {
// file_map
// file_content
// user_instructions
// }
return get(name: args_.name)!
}
// your checking & initialization code if needed
fn obj_init(mycfg_ HeropromptWorkspace) !HeropromptWorkspace {
@@ -55,16 +47,11 @@ fn obj_init(mycfg_ HeropromptWorkspace) !HeropromptWorkspace {
/////////////NORMALLY NO NEED TO TOUCH
// TODO: Check the compiler issue with the encde/decode
pub fn heroscript_dumps(obj HeropromptWorkspace) !string {
// return encoderhero.encode[HeropromptWorkspace](obj)! // temporarily commented out
return 'name: "${obj.name}"'
return encoderhero.encode[HeropromptWorkspace](obj)!
}
pub fn heroscript_loads(heroscript string) !HeropromptWorkspace {
// mut obj := encoderhero.decode[HeropromptWorkspace](heroscript)! // temporarily commented out
obj := HeropromptWorkspace{
name: 'default'
}
mut obj := encoderhero.decode[HeropromptWorkspace](heroscript)!
return obj
}

View File

@@ -1,10 +1,10 @@
!!heroprompt.configure name:"default"
!!heropromptworkspace.configure name:"default"
!!heroprompt.workspace_dir name:"default"
path:"@HOME/code/github/freeflowuniverse/herolib/lib/builder"
selection:"path1,path2" //paths are relative in the path of workspace
filter_exclude:","
filter_include:","
// !!heropromptworkspace.workspace_dir name:"default"
// path:"@HOME/code/github/freeflowuniverse/herolib/lib/builder"
// selection:"path1,path2" //paths are relative in the path of workspace
// filter_exclude:","
// filter_include:","

View File

@@ -1,171 +0,0 @@
<file_map>
/Users/mahmoud/code/github/freeflowuniverse/herolib
└── docker
└── docker_ubuntu_install.sh *
├── herolib
│ ├── .gitignore*
│ ├── build.sh *
│ └── debug.sh*
</file_map>
<file_contents>
File: /Users/mahmoud/code/github/freeflowuniverse/herolib/docker/docker_ubuntu_install.sh
```sh
#!/bin/bash
# Exit immediately if a command exits with a non-zero status
set -e
# Function to display an error message and exit
error_exit() {
echo "Error: $1" >&2
exit 1
}
# Update package index and upgrade system
echo "Updating system packages..."
sudo apt update && sudo apt upgrade -y || error_exit "Failed to update system packages."
# Install required packages for repository setup
echo "Installing prerequisites..."
sudo apt install -y ca-certificates curl gnupg || error_exit "Failed to install prerequisites."
# Create directory for Docker GPG key
echo "Setting up GPG keyring..."
sudo mkdir -p /etc/apt/keyrings || error_exit "Failed to create keyring directory."
# Add Docker's official GPG key
DOCKER_GPG_KEY=/etc/apt/keyrings/docker.gpg
echo "Adding Docker GPG key..."
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o $DOCKER_GPG_KEY || error_exit "Failed to add Docker GPG key."
sudo chmod a+r $DOCKER_GPG_KEY
# Set up Docker repository
echo "Adding Docker repository..."
REPO_ENTRY="deb [arch=$(dpkg --print-architecture) signed-by=$DOCKER_GPG_KEY] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
if ! grep -Fxq "$REPO_ENTRY" /etc/apt/sources.list.d/docker.list; then
echo "$REPO_ENTRY" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null || error_exit "Failed to add Docker repository."
fi
# Update package index
echo "Updating package index..."
sudo apt update || error_exit "Failed to update package index."
# Install Docker Engine, CLI, and dependencies
echo "Installing Docker Engine and dependencies..."
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin || error_exit "Failed to install Docker packages."
# Verify Docker installation
echo "Verifying Docker installation..."
if ! docker --version; then
error_exit "Docker installation verification failed."
fi
# Run a test container
echo "Running Docker test container..."
if ! sudo docker run --rm hello-world; then
error_exit "Docker test container failed to run."
fi
# Add current user to Docker group (if not already added)
echo "Configuring Docker group..."
if ! groups $USER | grep -q '\bdocker\b'; then
sudo usermod -aG docker $USER || error_exit "Failed to add user to Docker group."
echo "User added to Docker group. Please log out and back in for this change to take effect."
else
echo "User is already in the Docker group."
fi
# Enable Docker service on boot
echo "Enabling Docker service on boot..."
sudo systemctl enable docker || error_exit "Failed to enable Docker service."
# Success message
echo "Docker installation completed successfully!"
```
File: /Users/mahmoud/code/github/freeflowuniverse/herolib/docker/herolib/build.sh
```sh
#!/bin/bash -e
# Get the directory where the script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR"
# Copy installation files
cp ../../install_v.sh ./scripts/install_v.sh
cp ../../install_herolib.vsh ./scripts/install_herolib.vsh
# Docker image and container names
DOCKER_IMAGE_NAME="herolib"
DEBUG_CONTAINER_NAME="herolib"
function cleanup {
if docker ps -aq -f name="$DEBUG_CONTAINER_NAME" &>/dev/null; then
echo "Cleaning up leftover debug container..."
docker rm -f "$DEBUG_CONTAINER_NAME" &>/dev/null || true
fi
}
trap cleanup EXIT
# Attempt to build the Docker image
BUILD_LOG=$(mktemp)
set +e
docker build --name herolib --progress=plain -t "$DOCKER_IMAGE_NAME" .
BUILD_EXIT_CODE=$?
set -e
# Handle build failure
if [ $BUILD_EXIT_CODE -ne 0 ]; then
echo -e "\\n[ERROR] Docker build failed.\n"
echo -e "remove the part which didn't build in the Dockerfile, the run again and to debug do:"
echo docker run --name herolib -it --entrypoint=/bin/bash "herolib"
exit $BUILD_EXIT_CODE
else
echo -e "\\n[INFO] Docker build completed successfully."
fi
```
File: /Users/mahmoud/code/github/freeflowuniverse/herolib/docker/herolib/.gitignore
```
.bash_history
.openvscode-server/
.cache/
```
File: /Users/mahmoud/code/github/freeflowuniverse/herolib/docker/herolib/debug.sh
```sh
#!/bin/bash -ex
# Get the directory where the script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Remove any existing container named 'debug' (ignore errors)
docker rm -f herolib > /dev/null 2>&1
docker run --name herolib -it \
--entrypoint="/usr/local/bin/ourinit.sh" \
-v "${SCRIPT_DIR}/scripts:/scripts" \
-v "$HOME/code:/root/code" \
-p 4100:8100 \
-p 4101:8101 \
-p 4102:8102 \
-p 4379:6379 \
-p 4022:22 \
-p 4000:3000 herolib
```
</file_contents>
<user_instructions>
This is a small repo prompt example
</user_instructions>