fix(fn-run): propagar stdout/stderr de bash functions library-style #1

Open
dataforge wants to merge 537 commits from auto/0077-fn-run-bash-mudo into master
Showing only changes of commit f748256c1d - Show all commits
+202 -6
View File
@@ -37,6 +37,8 @@ func cmdOps(args []string) {
cmdOpsExecution(args[1:])
case "assertion":
cmdOpsAssertion(args[1:])
case "log":
cmdOpsLog(args[1:])
case "help", "-h", "--help":
printOpsUsage()
default:
@@ -87,7 +89,16 @@ Assertion commands:
fn ops assertion delete <id> Elimina assertion
fn ops assertion eval --entity-id <id> Evalua assertions activas
fn ops assertion result add <flags> Registra resultado manual
fn ops assertion result list [--assertion-id <id>]`)
fn ops assertion result list [--assertion-id <id>]
Log commands:
fn ops log add <flags> Registra log entry
fn ops log list [--level <lvl>] [--source <src>] [--limit N]
fn ops log show <id> Muestra log entry
Log flags:
--id <id> --level <debug|info|warn|error> --source <src>
--entity-id <id> --execution-id <id> --message <text> --metadata <json>`)
}
// --- ops init ---
@@ -714,12 +725,12 @@ func requireRegistryDB() *registry.DB {
// --- helpers ---
func findOpsDB() string {
func findOpsDB() (string, error) {
dir, _ := os.Getwd()
for {
path := filepath.Join(dir, opsDBName)
if _, err := os.Stat(path); err == nil {
return path
return path, nil
}
parent := filepath.Dir(dir)
if parent == dir {
@@ -727,15 +738,18 @@ func findOpsDB() string {
}
dir = parent
}
return filepath.Join(".", opsDBName)
return "", fmt.Errorf("operations.db not found (searched from %s up to /)\nRun 'fn ops init <path>' to create one inside an app directory", dir)
}
func openOpsDB() *ops.DB {
path := findOpsDB()
path, err := findOpsDB()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
db, err := ops.Open(path)
if err != nil {
fmt.Fprintf(os.Stderr, "error opening operations.db: %v\n", err)
fmt.Fprintln(os.Stderr, "Run 'fn ops init' first to create one.")
os.Exit(1)
}
return db
@@ -1398,6 +1412,188 @@ func formatResultValue(v map[string]any) string {
return s
}
// --- Log subcommands ---
func cmdOpsLog(args []string) {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "usage: fn ops log <add|list|show>")
os.Exit(1)
}
switch args[0] {
case "add":
cmdOpsLogAdd(args[1:])
case "list":
cmdOpsLogList(args[1:])
case "show":
cmdOpsLogShow(args[1:])
default:
fmt.Fprintf(os.Stderr, "unknown log command: %s\n", args[0])
os.Exit(1)
}
}
func cmdOpsLogAdd(args []string) {
var id, level, source, entityID, executionID, message, metadataStr string
i := 0
for i < len(args) {
switch args[i] {
case "--id":
i++
id = args[i]
case "--level":
i++
level = args[i]
case "--source":
i++
source = args[i]
case "--entity-id":
i++
entityID = args[i]
case "--execution-id":
i++
executionID = args[i]
case "--message", "-m":
i++
message = args[i]
case "--metadata":
i++
metadataStr = args[i]
}
i++
}
if message == "" {
// Read from stdin if no message flag
b, err := io.ReadAll(os.Stdin)
if err == nil && len(b) > 0 {
message = strings.TrimSpace(string(b))
}
}
if message == "" {
fmt.Fprintln(os.Stderr, "error: --message is required (or pipe to stdin)")
os.Exit(1)
}
if level == "" {
level = "info"
}
if id == "" {
id = fmt.Sprintf("log_%d", timeNow().UnixNano())
}
var metadata map[string]any
if metadataStr != "" {
if err := json.Unmarshal([]byte(metadataStr), &metadata); err != nil {
fmt.Fprintf(os.Stderr, "error: invalid metadata JSON: %v\n", err)
os.Exit(1)
}
}
l := &ops.Log{
ID: id,
Level: ops.LogLevel(level),
Source: source,
EntityID: entityID,
ExecutionID: executionID,
Message: message,
Metadata: metadata,
}
db := openOpsDB()
defer db.Close()
if err := db.InsertLog(l); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
fmt.Printf("Logged: [%s] %s\n", l.Level, truncate(l.Message, 60))
}
func cmdOpsLogList(args []string) {
var level, source, entityID, executionID string
limit := 50
i := 0
for i < len(args) {
switch args[i] {
case "--level":
i++
level = args[i]
case "--source":
i++
source = args[i]
case "--entity-id":
i++
entityID = args[i]
case "--execution-id":
i++
executionID = args[i]
case "--limit", "-n":
i++
limit = int(parseInt64(args[i]))
}
i++
}
db := openOpsDB()
defer db.Close()
logs, err := db.ListLogs(ops.LogLevel(level), source, entityID, executionID, limit)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if len(logs) == 0 {
fmt.Println("No logs.")
return
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "CREATED_AT\tLEVEL\tSOURCE\tMESSAGE")
for _, l := range logs {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", l.CreatedAt.Format(time.RFC3339), l.Level, l.Source, truncate(l.Message, 60))
}
w.Flush()
}
func cmdOpsLogShow(args []string) {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "usage: fn ops log show <id>")
os.Exit(1)
}
db := openOpsDB()
defer db.Close()
l, err := db.GetLog(args[0])
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if l == nil {
fmt.Fprintf(os.Stderr, "log %q not found\n", args[0])
os.Exit(1)
}
fmt.Printf("ID: %s\n", l.ID)
fmt.Printf("Level: %s\n", l.Level)
fmt.Printf("Source: %s\n", l.Source)
if l.EntityID != "" {
fmt.Printf("Entity: %s\n", l.EntityID)
}
if l.ExecutionID != "" {
fmt.Printf("Execution: %s\n", l.ExecutionID)
}
fmt.Printf("Message: %s\n", l.Message)
if len(l.Metadata) > 0 {
m, _ := json.MarshalIndent(l.Metadata, " ", " ")
fmt.Printf("Metadata: %s\n", string(m))
}
fmt.Printf("Created: %s\n", l.CreatedAt.Format(time.RFC3339))
}
func tryOpenRegistryDB() *registry.DB {
// Try FN_REGISTRY_ROOT env var first
if envRoot := os.Getenv("FN_REGISTRY_ROOT"); envRoot != "" {