This commit is contained in:
2025-04-23 04:18:28 +02:00
parent 10a7d9bb6b
commit a16ac8f627
276 changed files with 85166 additions and 1 deletions

View File

@@ -0,0 +1,38 @@
## handler factor
a handler factory has a function to register handlers
each handler has a name (what the actor is called)
each handler represents an actor with its actions
to understand more about heroscript which is the way how we can actor and actions see @instructions/knowledge/1_heroscript.md
and each handler has as function to translate heroscript to the implementation
the handler calls the required implementation (can be in one or more packages)
the handler has a play method which uses @pkg/heroscript/playbook to process heroscript and call the required implementation
create a folder in @pkg/heroscript/handlers which will have all the knowledge how to go from heroscript to implementation
## telnet server
we need a generic telnet server which takes a handler factory as input
the telnet server is very basic, it get's messages
each message is a heroscript
when an empty line is sent that means its the end of a heroscript message
the telnet server needs to be authenticated using a special heroscript message
!!auth secret:'secret123'
as long as that authentication has not been done it will not process any heroscript
the processing of heroscript happens by means of calling the handler factory
there can be more than one secret on the telnet server

View File

@@ -0,0 +1,435 @@
create a golang project
there will be multiple
- modules
- one is for installers
- one is for a fiber web server with a web ui, swagger UI and opeapi rest interface (v3.1.0 swagger)
- a generic redis server
- on the fiber webserver create multiple endpoints nicely structures as separate directories underneith the module
- executor (for executing commands, results in jobs)
- package manager (on basis of apt, brew, scoop)
- create an openapi interface for each of those v3.1.0
- integrate in generic way the goswagger interface so people can use the rest interface from web
- create a main server which connects to all the modules
### code for the redis server
```go
package main
import (
"fmt"
"log"
"strconv"
"strings"
"sync"
"time"
"github.com/tidwall/redcon"
)
// entry represents a stored value. For strings, value is stored as a string.
// For hashes, value is stored as a map[string]string.
type entry struct {
value interface{}
expiration time.Time // zero means no expiration
}
// Server holds the in-memory datastore and provides thread-safe access.
type Server struct {
mu sync.RWMutex
data map[string]*entry
}
// NewServer creates a new server instance and starts a cleanup goroutine.
func NewServer() *Server {
s := &Server{
data: make(map[string]*entry),
}
go s.cleanupExpiredKeys()
return s
}
// cleanupExpiredKeys periodically removes expired keys.
func (s *Server) cleanupExpiredKeys() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for range ticker.C {
now := time.Now()
s.mu.Lock()
for k, ent := range s.data {
if !ent.expiration.IsZero() && now.After(ent.expiration) {
delete(s.data, k)
}
}
s.mu.Unlock()
}
}
// set stores a key with a value and an optional expiration duration.
func (s *Server) set(key string, value interface{}, duration time.Duration) {
s.mu.Lock()
defer s.mu.Unlock()
var exp time.Time
if duration > 0 {
exp = time.Now().Add(duration)
}
s.data[key] = &entry{
value: value,
expiration: exp,
}
}
// get retrieves the value for a key if it exists and is not expired.
func (s *Server) get(key string) (interface{}, bool) {
s.mu.RLock()
ent, ok := s.data[key]
s.mu.RUnlock()
if !ok {
return nil, false
}
if !ent.expiration.IsZero() && time.Now().After(ent.expiration) {
// Key has expired; remove it.
s.mu.Lock()
delete(s.data, key)
s.mu.Unlock()
return nil, false
}
return ent.value, true
}
// del deletes a key and returns 1 if the key was present.
func (s *Server) del(key string) int {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.data[key]; ok {
delete(s.data, key)
return 1
}
return 0
}
// keys returns all keys matching the given pattern.
// For simplicity, only "*" is fully supported.
func (s *Server) keys(pattern string) []string {
s.mu.RLock()
defer s.mu.RUnlock()
var result []string
// Simple pattern matching: if pattern is "*", return all nonexpired keys.
if pattern == "*" {
for k, ent := range s.data {
if !ent.expiration.IsZero() && time.Now().After(ent.expiration) {
continue
}
result = append(result, k)
}
} else {
// For any other pattern, do a simple substring match.
for k, ent := range s.data {
if !ent.expiration.IsZero() && time.Now().After(ent.expiration) {
continue
}
if strings.Contains(k, pattern) {
result = append(result, k)
}
}
}
return result
}
// getHash retrieves the hash map stored at key.
func (s *Server) getHash(key string) (map[string]string, bool) {
v, ok := s.get(key)
if !ok {
return nil, false
}
hash, ok := v.(map[string]string)
return hash, ok
}
// hset sets a field in the hash stored at key. It returns 1 if the field is new.
func (s *Server) hset(key, field, value string) int {
s.mu.Lock()
defer s.mu.Unlock()
var hash map[string]string
ent, exists := s.data[key]
if exists {
if !ent.expiration.IsZero() && time.Now().After(ent.expiration) {
// expired; recreate a new hash.
hash = make(map[string]string)
s.data[key] = &entry{value: hash}
} else {
var ok bool
hash, ok = ent.value.(map[string]string)
if !ok {
// Overwrite if the key holds a non-hash value.
hash = make(map[string]string)
s.data[key] = &entry{value: hash}
}
}
} else {
hash = make(map[string]string)
s.data[key] = &entry{value: hash}
}
_, fieldExists := hash[field]
hash[field] = value
if fieldExists {
return 0
}
return 1
}
// hget retrieves the value of a field in the hash stored at key.
func (s *Server) hget(key, field string) (string, bool) {
hash, ok := s.getHash(key)
if !ok {
return "", false
}
val, exists := hash[field]
return val, exists
}
// hdel deletes one or more fields from the hash stored at key.
// Returns the number of fields that were removed.
func (s *Server) hdel(key string, fields []string) int {
hash, ok := s.getHash(key)
if !ok {
return 0
}
count := 0
for _, field := range fields {
if _, exists := hash[field]; exists {
delete(hash, field)
count++
}
}
return count
}
// hkeys returns all field names in the hash stored at key.
func (s *Server) hkeys(key string) []string {
hash, ok := s.getHash(key)
if !ok {
return nil
}
var keys []string
for field := range hash {
keys = append(keys, field)
}
return keys
}
// hlen returns the number of fields in the hash stored at key.
func (s *Server) hlen(key string) int {
hash, ok := s.getHash(key)
if !ok {
return 0
}
return len(hash)
}
// incr increments the integer value stored at key by one.
// If the key does not exist, it is set to 0 before performing the operation.
func (s *Server) incr(key string) (int64, error) {
s.mu.Lock()
defer s.mu.Unlock()
var current int64
ent, exists := s.data[key]
if exists {
if !ent.expiration.IsZero() && time.Now().After(ent.expiration) {
current = 0
} else {
switch v := ent.value.(type) {
case string:
var err error
current, err = strconv.ParseInt(v, 10, 64)
if err != nil {
return 0, err
}
case int:
current = int64(v)
case int64:
current = v
default:
return 0, fmt.Errorf("value is not an integer")
}
}
}
current++
// Store the new value as a string.
s.data[key] = &entry{
value: strconv.FormatInt(current, 10),
}
return current, nil
}
func main() {
server := NewServer()
log.Println("Starting Redis-like server on :6379")
err := redcon.ListenAndServe(":6379",
func(conn redcon.Conn, cmd redcon.Command) {
// Every command is expected to have at least one argument (the command name).
if len(cmd.Args) == 0 {
conn.WriteError("ERR empty command")
return
}
command := strings.ToLower(string(cmd.Args[0]))
switch command {
case "ping":
conn.WriteString("PONG")
case "set":
// Usage: SET key value [EX seconds]
if len(cmd.Args) < 3 {
conn.WriteError("ERR wrong number of arguments for 'set' command")
return
}
key := string(cmd.Args[1])
value := string(cmd.Args[2])
duration := time.Duration(0)
// Check for an expiration option (only EX is supported here).
if len(cmd.Args) > 3 {
if strings.ToLower(string(cmd.Args[3])) == "ex" && len(cmd.Args) > 4 {
seconds, err := strconv.Atoi(string(cmd.Args[4]))
if err != nil {
conn.WriteError("ERR invalid expire time")
return
}
duration = time.Duration(seconds) * time.Second
}
}
server.set(key, value, duration)
conn.WriteString("OK")
case "get":
if len(cmd.Args) < 2 {
conn.WriteError("ERR wrong number of arguments for 'get' command")
return
}
key := string(cmd.Args[1])
v, ok := server.get(key)
if !ok {
conn.WriteNull()
return
}
// Only string type is returned by GET.
switch val := v.(type) {
case string:
conn.WriteBulkString(val)
default:
conn.WriteError("WRONGTYPE Operation against a key holding the wrong kind of value")
}
case "del":
if len(cmd.Args) < 2 {
conn.WriteError("ERR wrong number of arguments for 'del' command")
return
}
count := 0
for i := 1; i < len(cmd.Args); i++ {
key := string(cmd.Args[i])
count += server.del(key)
}
conn.WriteInt(count)
case "keys":
if len(cmd.Args) < 2 {
conn.WriteError("ERR wrong number of arguments for 'keys' command")
return
}
pattern := string(cmd.Args[1])
keys := server.keys(pattern)
res := make([][]byte, len(keys))
for i, k := range keys {
res[i] = []byte(k)
}
conn.WriteArray(res)
case "hset":
// Usage: HSET key field value
if len(cmd.Args) < 4 {
conn.WriteError("ERR wrong number of arguments for 'hset' command")
return
}
key := string(cmd.Args[1])
field := string(cmd.Args[2])
value := string(cmd.Args[3])
added := server.hset(key, field, value)
conn.WriteInt(added)
case "hget":
// Usage: HGET key field
if len(cmd.Args) < 3 {
conn.WriteError("ERR wrong number of arguments for 'hget' command")
return
}
key := string(cmd.Args[1])
field := string(cmd.Args[2])
v, ok := server.hget(key, field)
if !ok {
conn.WriteNull()
return
}
conn.WriteBulkString(v)
case "hdel":
// Usage: HDEL key field [field ...]
if len(cmd.Args) < 3 {
conn.WriteError("ERR wrong number of arguments for 'hdel' command")
return
}
key := string(cmd.Args[1])
fields := make([]string, 0, len(cmd.Args)-2)
for i := 2; i < len(cmd.Args); i++ {
fields = append(fields, string(cmd.Args[i]))
}
removed := server.hdel(key, fields)
conn.WriteInt(removed)
case "hkeys":
// Usage: HKEYS key
if len(cmd.Args) < 2 {
conn.WriteError("ERR wrong number of arguments for 'hkeys' command")
return
}
key := string(cmd.Args[1])
fields := server.hkeys(key)
res := make([][]byte, len(fields))
for i, field := range fields {
res[i] = []byte(field)
}
conn.WriteArray(res)
case "hlen":
// Usage: HLEN key
if len(cmd.Args) < 2 {
conn.WriteError("ERR wrong number of arguments for 'hlen' command")
return
}
key := string(cmd.Args[1])
length := server.hlen(key)
conn.WriteInt(length)
case "incr":
if len(cmd.Args) < 2 {
conn.WriteError("ERR wrong number of arguments for 'incr' command")
return
}
key := string(cmd.Args[1])
newVal, err := server.incr(key)
if err != nil {
conn.WriteError("ERR " + err.Error())
return
}
conn.WriteInt64(newVal)
default:
conn.WriteError("ERR unknown command '" + command + "'")
}
},
// Accept connection: always allow.
func(conn redcon.Conn) bool { return true },
// On connection close.
func(conn redcon.Conn, err error) {},
)
if err != nil {
log.Fatal(err)
}
}
```
test above code, test with a redis client it works

View File

@@ -0,0 +1,78 @@
there is a module called doctree
the metadata info of doctree is stored in a redis server
there is the concept of collection
a collection has markdown pages
each page has a unique name in the collection
a collection has files which can be a sstd file or image
each file has a unique name
the names are lower_cased and all non ascci chars are removed, use the namefix function as used in internal/tools/name_fix_test.go
its a struct called DocTree
which has as argument a path and a name which is also namefixed
the init walks over the path and finds all files and .md files
we remember the relative position of each file and markdown page in a hset
hset is:
- key: collections:$name
- hkey: pagename.md (always namefixed)
- hkey: imagename.png ... or any other extension for files (always namefixed)
- val: the relative position in the doctree location
use redisclient to internal redis to store this
create following methods on doctree
- Scan (scan the collection again, remove hset and repopulate)
- PageGet get page from a name (do namefix inside method) return the markdown
- PageGetHtml same as PageGet but make html
- FileGetUrl the url which can then be used in static webserver for downloading this content
- PageGetPath relative path in the collection
- Info (name & path)
in PageGet implement a simple include function which is done as !!include name:'pagename' this needs to include page as mentioned in this collection
if !!include name:'othercollection:pagename' then pagename comes from other collection do namefix to find
## Objects
#### DocTree
- has add, get, delete, list functions in relation to underlying Collection
### Collection
- has get/set/delete/list for pages
- has get/set/delete/list for files
namefix used everywhere to make sure
- in get for page we do the include which an get other pages
## special functions
### Include
```
!!include collectionname:'pagename'
!!include collectionname:'pagename.md'
!!include 'pagename'
!!include collectionname:pagename
!!include collectionname:pagename.md
```
the include needs to parse the following
note:
- pages can have .md or not, check if given if not add
- all is namefixed on collection and page level
- there can be '' around the name but this is optional

View File

@@ -0,0 +1,9 @@
create a client for processmanager over telnet in @pkg/handlerfactory/clients
this is over tcp or socket
make a factory in clients where we say to which server we want to connect and then make methods per action which have right parameters which then create the heroscript which is sent to the server
we always expect json back
the json gets parsed and returned

View File

@@ -0,0 +1,28 @@
create a pkg/imapserver lib which uses:
https://github.com/foxcpp/go-imap
the mails are in redis
the model for mail is in @pkg/mail/model.go
## the mails are in redis based on following code, learn from it
cmd/redis_mail_feeder/main.go
the redis keys are
- mail:in:$account:$folder:$uid
the json is the mail model
see @instructions_imap_feeder.md for details
## imap server is using the redis as backedn
- based on what the feeder put in
there is no no login/passwd, anything is fine, any authentication is fine,
ignore if user specifies it, try to support any login/passwd/authentication method just accept everything

View File

@@ -0,0 +1,23 @@
## populator of imap
in @/cmd/
create a new command called redis_mail_feeder
this feeder creates 100 mails in different folders and stores them in redis as datastor
the mail model is in @pkg/mail/model.go
@uid is epoch in seconds + an incrementing number based on of there was already a mail with the same uid before, so we just increment the number until we get a unique uid (is string(epoch)+string(incrementing number))
the mails are stored in
and stores mail in mail:in:$account:$folder:$uid
id is the blake192 from the json serialization
- account is random over pol & jan
- folders chose then random can be upto 3 levels deep
make random emails, 100x in well chosen folder

View File

@@ -0,0 +1,4 @@
the @pkg/mcpopenapi/cmd/mcpopenapi works very welll
we want to implement

View File

@@ -0,0 +1,236 @@
# OpenAPI Generation Instructions
## Overview
The OpenAPI package in `pkg/openapi` provides functionality to generate server code from OpenAPI specifications. This document explains how to use this package to generate and host multiple APIs under a single server with Swagger UI integration.
## Implementation Status
We have successfully implemented:
1. A proper test in `pkg/openapi/examples` that generates code from OpenAPI specifications
2. Code generation for two example APIs:
- `petstoreapi` (from `petstore.yaml`)
- `actionsapi` (from `actions.yaml`)
3. A webserver that hosts multiple generated APIs
4. Swagger UI integration for API documentation
5. A home page with links to the APIs and their documentation
All APIs are hosted under `$serverurl:$port/api` with a clean navigation structure.
## Directory Structure
```
pkg/openapi/
├── examples/
│ ├── actions.yaml # OpenAPI spec for Actions API
│ ├── actionsapi/ # Generated code for Actions API
│ ├── main.go # Main server implementation
│ ├── petstore.yaml # OpenAPI spec for Petstore API
│ ├── petstoreapi/ # Generated code for Petstore API
│ ├── README.md # Documentation for examples
│ ├── run_test.sh # Script to run tests and server
│ └── test/ # Tests for OpenAPI generation
├── generator.go # Server code generator
├── parser.go # OpenAPI spec parser
├── example.go # Example usage
└── templates/ # Code generation templates
└── server.tmpl # Server template
```
## How to Use
### Running the Example
To run the example implementation:
1. Navigate to the examples directory:
```bash
cd pkg/openapi/examples
```
2. Run the test script:
```bash
./run_test.sh
```
3. Access the APIs:
- API Home: http://localhost:9091/api
- Petstore API: http://localhost:9091/api/petstore
- Petstore API Documentation: http://localhost:9091/api/swagger/petstore
- Actions API: http://localhost:9091/api/actions
- Actions API Documentation: http://localhost:9091/api/swagger/actions
### Generating Code from Your Own OpenAPI Spec
To generate code from your own OpenAPI specification:
```go
package main
import (
"fmt"
"os"
"path/filepath"
"github.com/freeflowuniverse/heroagent/pkg/openapi"
)
func main() {
// Parse the OpenAPI specification
spec, err := openapi.ParseFromFile("your-api.yaml")
if err != nil {
fmt.Printf("Failed to parse OpenAPI specification: %v\n", err)
os.Exit(1)
}
// Create a server generator
generator := openapi.NewServerGenerator(spec)
// Generate server code
serverCode := generator.GenerateServerCode()
// Write the server code to a file
outputPath := "generated-server.go"
err = os.WriteFile(outputPath, []byte(serverCode), 0644)
if err != nil {
fmt.Printf("Failed to write server code: %v\n", err)
os.Exit(1)
}
fmt.Printf("Generated server code in %s\n", outputPath)
}
```
### Hosting Multiple APIs
To host multiple APIs under a single server:
```go
package main
import (
"fmt"
"github.com/freeflowuniverse/heroagent/pkg/openapi"
"github.com/gofiber/fiber/v2"
)
func main() {
// Create the main server
app := fiber.New()
// Setup API routes
app.Get("/api", func(c *fiber.Ctx) error {
return c.SendString("API Home Page")
})
// Mount the first API
spec1, _ := openapi.ParseFromFile("api1.yaml")
generator1 := openapi.NewServerGenerator(spec1)
apiServer1 := generator1.GenerateServer()
app.Mount("/api/api1", apiServer1)
// Mount the second API
spec2, _ := openapi.ParseFromFile("api2.yaml")
generator2 := openapi.NewServerGenerator(spec2)
apiServer2 := generator2.GenerateServer()
app.Mount("/api/api2", apiServer2)
// Start the server
app.Listen(":8080")
}
```
### Adding Swagger UI
To add Swagger UI for API documentation:
```go
// Serve OpenAPI specs
app.Static("/api/api1/openapi.yaml", "api1.yaml")
app.Static("/api/api2/openapi.yaml", "api2.yaml")
// API1 Swagger UI
app.Get("/api/swagger/api1", func(c *fiber.Ctx) error {
return c.SendString(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>API1 - Swagger UI</title>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui.css" />
<style>
html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }
*, *:before, *:after { box-sizing: inherit; }
body { margin: 0; background: #fafafa; }
.swagger-ui .topbar { display: none; }
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-standalone-preset.js"></script>
<script>
window.onload = function() {
const ui = SwaggerUIBundle({
url: "/api/api1/openapi.yaml",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
layout: "StandaloneLayout"
});
window.ui = ui;
};
</script>
</body>
</html>
`)
})
```
## Features
### OpenAPI Parsing
The package can parse OpenAPI 3.0 and 3.1 specifications from files or byte slices.
### Code Generation
The package generates Fiber server code with mock implementations based on examples in the OpenAPI spec.
### Mock Implementations
Mock implementations are created using examples from the OpenAPI spec, making it easy to test APIs without writing any code.
### Multiple API Hosting
The package supports hosting multiple APIs under a single server, with each API mounted at a different path.
### Swagger UI Integration
The package includes Swagger UI integration for API documentation, making it easy to explore and test APIs.
## Best Practices
1. **Organize Your Code**: Keep your OpenAPI specs, generated code, and server implementation in separate directories.
2. **Use Examples**: Include examples in your OpenAPI spec to generate better mock implementations.
3. **Test Your APIs**: Write tests to verify that your APIs work as expected.
4. **Document Your APIs**: Use Swagger UI to document your APIs and make them easier to use.
5. **Use Version Control**: Keep your OpenAPI specs and generated code in version control to track changes.
## Troubleshooting
- **Parse Error**: If you get a parse error, check that your OpenAPI spec is valid. You can use tools like [Swagger Editor](https://editor.swagger.io/) to validate your spec.
- **Generation Error**: If code generation fails, check that your OpenAPI spec includes all required fields and that examples are properly formatted.
- **Server Error**: If the server fails to start, check that the port is not already in use and that all required dependencies are installed.

View File

@@ -0,0 +1,23 @@
in @pkg/system/stats
create a factory which is called StatsManager
then each method in the different files is a method on that StatsManager
then on StatsManager make a connection to a redis server and have this connection as property
then have a dict on the StatsManager called Expiration which in seconds defines per type of info how long we will cache
then on each method cache the info in redis on a well chosen key, if someone asks the info and its out of expiration then we send message to the goroutine which fetches the info (see further) for the next request, in other words we will stil give the info we had in cache but next request would then have the new info if it got fetched in time
make a goroutine which is doing the updates, only when info is asked for it will request it,
have an internal queue which tells this go-routine which info to ask for, NO info can be asked in parallel its one after the other
when an external consumer asks for the info it always comes from the cache
when the system starts up, the goroutine will do a first fetch, so in background initial info is loaded
when a request is asked from external consumer and info is not there yet, the method wll keep on polling redis this info is there (block wait), this redis will only be filled in once the goroutine fetches it
there is a generic timeout of 1 min
put a debug flag (bool) on the StatsManager if that one is set then the request for stats is always direct, not waiting for cache and no go-routine used

View File

@@ -0,0 +1,100 @@
create a process manager
which keeps separate process under control, measures the used cpu, memory
add possibilities to list, create, delete
we can talk to the process manager over local unix domain socket using a telnet session
## authentication
the telnet server says:
** Welcome: you are not authenticated, provide secret.
then we pass the secret which was passed when we started the process manager
once authenticated it says
** Welcome: you are authenticated.
now we can send heroscripts to it (see @pkg/playbook for how to parse that)
## actions can be sent over telnet
just send heroscript statements
everytime a new !! goes or # as comment we execute the previous heroscripts
## we make handlers
using the playbook: @pkg/playbook
this checks which commands are sent and this then calls the corresponding handler and instructs the processmanager
## start
heroscript
```bash
!!process.start name:'processname' command:'command\n which can be multiline' log:true
deadline:30 cron:'0 0 * * *' jobid:'e42'
```
## list
heroscript
```bash
!!process.list format:json
```
lists the processes and returns as json
when telnet protocol needs to return its always as
**RESULT** e42
... here is the result in chosen format
**ENDRESULT**
if jobid specified on the heroscript action then its shown behind **RESULT** if not then its empty
## delete
```bash
!!process.delete name:'processname'
```
## status
```bash
!!process.status name:'processname' format:json
```
shows mem usage, cpu usage, status e.g. running ...
## restart, stop, start
do same as status but then for these
## log
```bash
!!process.log name:'processname' format:json limit:100
```
returns the last 100 lines of the log
if not format then just the log itself

View File

@@ -0,0 +1,17 @@
create a pkg/smtp lib based on
https://github.com/emersion/go-smtp/tree/master
each mail coming in need to be converted to unicode text
and stored as json with
from
to
subject
message
attachments []Attachment
Attachment = encoded binary
into the local redis as hset and in a queue called mail:out which has has the unique id of the mssage
hset is mail:out:$unid -> the json

View File

@@ -0,0 +1,9 @@
in @pkg/sysstats
create a factory to SysStats
which has following methods
- memory
- cpu % used

View File

@@ -0,0 +1,23 @@
in @pkg/lang
create a vlangprocessor struct which will have some functions
first function is get_spect(path)
which walks over the path -recursive and finds all .v files
then it will process each of these files
in each file we will look for public structs and public methods on those structs
then return a script which only has
the Struct...
and then the methods on the structs
BUT NO CODE INSIDE THE METHODS
basically the returned codeis just Structs and Methods without the code
documentation is maintained
test on /Users/despiegk/code/github/freeflowuniverse/herolib/lib/circles/core

View File

@@ -0,0 +1,172 @@
# WebDAV Server Implementation
This document describes the WebDAV server implementation for HeroLauncher.
## Overview
The WebDAV server provides a way to access and manage files through the WebDAV protocol, which allows for remote file management over HTTP/HTTPS. This implementation uses the Go standard library's WebDAV package from `golang.org/x/net/webdav`.
The server supports both HTTP and HTTPS connections, basic authentication, and includes comprehensive debug logging for troubleshooting.
## Implementation Details
The WebDAV server is implemented in the `pkg/webdavserver` package. The server can be configured with various options including:
- Host and port to listen on
- Base path for the WebDAV endpoint
- File system path to serve files from
- Read and write timeouts
- Debug mode for verbose logging
- Basic authentication with username/password
- HTTPS support with TLS certificate and key files
## Usage
### Starting the WebDAV Server
To start the WebDAV server, use the `cmd/webdavserver/main.go` command:
```bash
go run cmd/webdavserver/main.go [options]
```
Available options:
- `-host`: Host address to bind to (default: "0.0.0.0")
- `-port`: Port to listen on (default: 9999)
- `-base-path`: Base URL path for WebDAV (default: "/")
- `-fs`: File system path to serve (default: system temp directory + "/heroagent")
- `-debug`: Enable debug mode with verbose logging (default: false)
- `-auth`: Enable basic authentication (default: false)
- `-username`: Username for basic authentication (default: "admin")
- `-password`: Password for basic authentication (default: "1234")
- `-https`: Enable HTTPS (default: false)
- `-cert`: Path to TLS certificate file (optional if auto-generation is enabled)
- `-key`: Path to TLS key file (optional if auto-generation is enabled)
- `-auto-gen-certs`: Auto-generate certificates if they don't exist (default: true)
- `-cert-validity`: Validity period in days for auto-generated certificates (default: 365)
- `-cert-org`: Organization name for auto-generated certificates (default: "HeroLauncher WebDAV Server")
### Connecting to WebDAV from macOS
A bash script is provided to easily connect to the WebDAV server from macOS:
```bash
./scripts/open_webdav_osx.sh [options]
```
Available options:
- `-h, --host`: WebDAV server hostname (default: "localhost")
- `-p, --port`: WebDAV server port (default: 9999)
- `-path, --path-prefix`: Path prefix for WebDAV URL (default: "")
- `-s, --https`: Use HTTPS instead of HTTP (default: false)
- `-u, --username`: Username for authentication
- `-pw, --password`: Password for authentication
- `--help`: Show help message
## API
### Server Configuration
```go
// Config holds the configuration for the WebDAV server
type Config struct {
Host string
Port int
BasePath string
FileSystem string
ReadTimeout time.Duration
WriteTimeout time.Duration
DebugMode bool
UseAuth bool
Username string
Password string
UseHTTPS bool
CertFile string
KeyFile string
AutoGenerateCerts bool
CertValidityDays int
CertOrganization string
}
// DefaultConfig returns the default configuration
func DefaultConfig() Config
```
### Server Methods
```go
// NewServer creates a new WebDAV server
func NewServer(config Config) (*Server, error)
// Start starts the WebDAV server
func (s *Server) Start() error
// Stop stops the WebDAV server
func (s *Server) Stop() error
```
## Integration with HeroLauncher
The WebDAV server can be integrated with the main HeroLauncher application by adding it to the server initialization in `cmd/server/main.go`.
## Directory Structure
The WebDAV server uses the following directory structure:
```
<parent-of-fs>/
├── <fs-dir>/ # WebDAV files served to clients (specified by -fs)
└── certificates/ # TLS certificates for HTTPS
```
Where certificates are stored in a `certificates` directory next to the filesystem directory specified with the `-fs` parameter.
## Security Considerations
- Basic authentication is supported but disabled by default
- HTTPS is supported but disabled by default
- The server can automatically generate self-signed certificates if needed
- For production use, always enable authentication and HTTPS
- Use strong passwords and properly signed certificates for production
- Be careful about which directories you expose through WebDAV
- Consider implementing IP-based access restrictions for additional security
## Debugging
When troubleshooting WebDAV connections, the debug mode can be enabled with the `-debug` flag. This will provide detailed logging of:
- All incoming requests
- Request headers
- Client information
- Authentication attempts
- WebDAV operations
Debug logs are prefixed with `[WebDAV DEBUG]` for easy filtering.
## Examples
### Starting a secure WebDAV server with auto-generated certificates
```bash
go run cmd/webdavserver/main.go -auth -username myuser -password mypass -https -fs /path/to/files -debug
```
### Starting a secure WebDAV server with existing certificates
```bash
go run cmd/webdavserver/main.go -auth -username myuser -password mypass -https -cert /path/to/cert.pem -key /path/to/key.pem -fs /path/to/files -debug -auto-gen-certs=false
```
### Connecting from macOS with authentication
```bash
./scripts/open_webdav_osx.sh -s -u myuser -pw mypass
```
## References
- [WebDAV Protocol (RFC 4918)](https://tools.ietf.org/html/rfc4918)
- [Go WebDAV Package](https://pkg.go.dev/golang.org/x/net/webdav)
- [TLS in Go](https://pkg.go.dev/crypto/tls)

View File

@@ -0,0 +1,10 @@
in pkg/handlerfactory
create a handler for the process manager
the code of process manager is in pkg/processmanager/processmanager.go
make a directory per handler and call processmanager underneith handlerfactory
inspiration how to do it comes from pkg/handlerfactory/cmd/vmhandler/vm_handler.go
how to use heroscript is in pkg/heroscript

View File

@@ -0,0 +1,78 @@
# HeroScript
## Overview
HeroScript is a simple, declarative scripting language designed to define workflows and execute commands in a structured manner. It follows a straightforward syntax where each action is prefixed with `!!`, indicating the actor and action name.
## Example
A basic HeroScript script for virtual machine management looks like this:
```heroscript
!!vm.define name:'test_vm' cpu:4
memory: '8GB'
storage: '100GB'
description: '
A virtual machine configuration
with specific resources.
'
!!vm.start name:'test_vm'
!!vm.disk_add
name: 'test_vm'
size: '50GB'
type: 'SSD'
!!vm.delete
name: 'test_vm'
force: true
```
### Key Features
- Every action starts with `!!`.
- The first part after `!!` is the actor (e.g., `vm`).
- The second part is the action name (e.g., `define`, `start`, `delete`).
- Multi-line values are supported (e.g., the `description` field).
- Lists are comma-separated where applicable and inside ''.
- If items one 1 line, then no space between name & argument e.g. name:'test_vm'
## Parsing HeroScript
Internally, HeroScript gets parsed into an action object with parameters. Each parameter follows a `key: value` format.
### Parsing Example
```heroscript
!!actor.action
id:a1 name6:aaaaa
name:'need to do something 1'
description:
'
## markdown works in it
description can be multiline
lets see what happens
- a
- something else
### subtitle
'
name2: test
name3: hi
name10:'this is with space' name11:aaa11
name4: 'aaa'
//somecomment
name5: 'aab'
```
### Parsing Details
- Each parameter follows a `key: value` format.
- Multi-line values (such as descriptions) support Markdown formatting.
- Comments can be added using `//`.
- Keys and values can have spaces, and values can be enclosed in single quotes.

View File

@@ -0,0 +1,267 @@
## how to process heroscript in Vlang
- heroscript can be converted to a struct,
- the methods available to get the params are in 'params' section further in this doc
```vlang
fn test_play_dagu() ! {
mut plbook := playbook.new(text: thetext_from_above)!
play_dagu(mut plbook)! //see below in vlang block there it all happens
}
pub fn play_dagu(mut plbook playbook.PlayBook) ! {
//find all actions are !!$actor.$actionname. in this case above the actor is !!dagu, we check with the fitler if it exists, if not we return
dagu_actions := plbook.find(filter: 'dagu.')!
if dagu_actions.len == 0 {
return
}
play_dagu_basic(mut plbook)!
}
pub struct DaguScript {
pub mut:
name string
homedir string
title string
reset bool
start bool
colors []string
}
// play_dagu plays the dagu play commands
pub fn play_dagu_basic(mut plbook playbook.PlayBook) ! {
//now find the specific ones for dagu.script_define
mut actions := plbook.find(filter: 'dagu.script_define')!
if actions.len > 0 {
for myaction in actions {
mut p := myaction.params //get the params object from the action object, this can then be processed using the param getters
mut obj := DaguScript{
//INFO: all details about the get methods can be found in 'params get methods' section
name : p.get('name')! //will give error if not exist
homedir : p.get('homedir')!
title : p.get_default('title', 'My Hero DAG')! //uses a default if not set
reset : p.get_default_false('reset')
start : p.get_default_true('start')
colors : p.get_list('colors')
description : p.get_default('description','')!
}
...
}
}
//there can be more actions which will have other filter
}
```
## params get methods (param getters)
```vlang
fn (params &Params) exists(key_ string) bool
//check if arg exist (arg is just a value in the string e.g. red, not value:something)
fn (params &Params) exists_arg(key_ string) bool
//see if the kwarg with the key exists if yes return as string trimmed
fn (params &Params) get(key_ string) !string
//return the arg with nr, 0 is the first
fn (params &Params) get_arg(nr int) !string
//return arg, if the nr is larger than amount of args, will return the defval
fn (params &Params) get_arg_default(nr int, defval string) !string
fn (params &Params) get_default(key string, defval string) !string
fn (params &Params) get_default_false(key string) bool
fn (params &Params) get_default_true(key string) bool
fn (params &Params) get_float(key string) !f64
fn (params &Params) get_float_default(key string, defval f64) !f64
fn (params &Params) get_from_hashmap(key_ string, defval string, hashmap map[string]string) !string
fn (params &Params) get_int(key string) !int
fn (params &Params) get_int_default(key string, defval int) !int
//Looks for a list of strings in the parameters. ',' are used as deliminator to list
fn (params &Params) get_list(key string) ![]string
fn (params &Params) get_list_default(key string, def []string) ![]string
fn (params &Params) get_list_f32(key string) ![]f32
fn (params &Params) get_list_f32_default(key string, def []f32) []f32
fn (params &Params) get_list_f64(key string) ![]f64
fn (params &Params) get_list_f64_default(key string, def []f64) []f64
fn (params &Params) get_list_i16(key string) ![]i16
fn (params &Params) get_list_i16_default(key string, def []i16) []i16
fn (params &Params) get_list_i64(key string) ![]i64
fn (params &Params) get_list_i64_default(key string, def []i64) []i64
fn (params &Params) get_list_i8(key string) ![]i8
fn (params &Params) get_list_i8_default(key string, def []i8) []i8
fn (params &Params) get_list_int(key string) ![]int
fn (params &Params) get_list_int_default(key string, def []int) []int
fn (params &Params) get_list_namefix(key string) ![]string
fn (params &Params) get_list_namefix_default(key string, def []string) ![]string
fn (params &Params) get_list_u16(key string) ![]u16
fn (params &Params) get_list_u16_default(key string, def []u16) []u16
fn (params &Params) get_list_u32(key string) ![]u32
fn (params &Params) get_list_u32_default(key string, def []u32) []u32
fn (params &Params) get_list_u64(key string) ![]u64
fn (params &Params) get_list_u64_default(key string, def []u64) []u64
fn (params &Params) get_list_u8(key string) ![]u8
fn (params &Params) get_list_u8_default(key string, def []u8) []u8
fn (params &Params) get_map() map[string]string
fn (params &Params) get_path(key string) !string
fn (params &Params) get_path_create(key string) !string
fn (params &Params) get_percentage(key string) !f64
fn (params &Params) get_percentage_default(key string, defval string) !f64
//convert GB, MB, KB to bytes e.g. 10 GB becomes bytes in u64
fn (params &Params) get_storagecapacity_in_bytes(key string) !u64
fn (params &Params) get_storagecapacity_in_bytes_default(key string, defval u64) !u64
fn (params &Params) get_storagecapacity_in_gigabytes(key string) !u64
//Get Expiration object from time string input input can be either relative or absolute## Relative time
fn (params &Params) get_time(key string) !ourtime.OurTime
fn (params &Params) get_time_default(key string, defval ourtime.OurTime) !ourtime.OurTime
fn (params &Params) get_time_interval(key string) !Duration
fn (params &Params) get_timestamp(key string) !Duration
fn (params &Params) get_timestamp_default(key string, defval Duration) !Duration
fn (params &Params) get_u32(key string) !u32
fn (params &Params) get_u32_default(key string, defval u32) !u32
fn (params &Params) get_u64(key string) !u64
fn (params &Params) get_u64_default(key string, defval u64) !u64
fn (params &Params) get_u8(key string) !u8
fn (params &Params) get_u8_default(key string, defval u8) !u8
```
## how internally a heroscript gets parsed for params
- example to show how a heroscript gets parsed in action with params
- params are part of action object
```heroscript
example text to parse (heroscript)
id:a1 name6:aaaaa
name:'need to do something 1'
description:
'
## markdown works in it
description can be multiline
lets see what happens
- a
- something else
### subtitle
'
name2: test
name3: hi
name10:'this is with space' name11:aaa11
name4: 'aaa'
//somecomment
name5: 'aab'
```
the params are part of the action and are represented as follow for the above:
```vlang
Params{
params: [Param{
key: 'id'
value: 'a1'
}, Param{
key: 'name6'
value: 'aaaaa'
}, Param{
key: 'name'
value: 'need to do something 1'
}, Param{
key: 'description'
value: '## markdown works in it
description can be multiline
lets see what happens
- a
- something else
### subtitle
'
}, Param{
key: 'name2'
value: 'test'
}, Param{
key: 'name3'
value: 'hi'
}, Param{
key: 'name10'
value: 'this is with space'
}, Param{
key: 'name11'
value: 'aaa11'
}, Param{
key: 'name4'
value: 'aaa'
}, Param{
key: 'name5'
value: 'aab'
}]
}
```