Plans 1-5 complete for core/go scope. 456 tests, 84.4% coverage, 100% AX-7 naming.
Critical bugs (Plan 1):
- P4-3+P7-3: ACTION broadcast calls all handlers with panic recovery
- P7-2+P7-4: RunE() with defer ServiceShutdown, Run() delegates
- P3-1: Startable/Stoppable return Result (breaking, clean)
- P9-1: Zero os/exec — App.Find() rewritten with os.Stat+PATH
- I3: Embed() removed, I15: New() comment fixed
- I9: CommandLifecycle removed → Command.Managed field
Registry[T] (Plan 2):
- Universal thread-safe named collection with 3 lock modes
- All 5 registries migrated: services, commands, drive, data, lock
- Insertion order preserved (fixes P4-1)
- c.RegistryOf("name") cross-cutting accessor
Action/Task system (Plan 3):
- Action type with Run()/Exists(), ActionHandler signature
- c.Action("name") dual-purpose accessor (register/invoke)
- TaskDef with Steps — sequential chain, async dispatch, previous-input piping
- Panic recovery on all Action execution
- broadcast() internal, ACTION() sugar
Process primitive (Plan 4):
- c.Process() returns Action sugar — Run/RunIn/RunWithEnv/Start/Kill/Exists
- No deps added — delegates to c.Action("process.*")
- Permission-by-registration: no handler = no capability
Missing primitives (Plan 5):
- core.ID() — atomic counter + crypto/rand suffix
- ValidateName() / SanitisePath() — reusable validation
- Fs.WriteAtomic() — write-to-temp-then-rename
- Fs.NewUnrestricted() / Fs.Root() — legitimate sandbox bypass
- AX-7: 456/456 tests renamed to TestFile_Function_{Good,Bad,Ugly}
Co-Authored-By: Virgil <virgil@lethean.io>
93 lines
2.5 KiB
Go
93 lines
2.5 KiB
Go
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
// Application identity for the Core framework.
|
|
|
|
package core
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// App holds the application identity and optional GUI runtime.
|
|
//
|
|
// app := core.App{}.New(core.NewOptions(
|
|
// core.Option{Key: "name", Value: "Core CLI"},
|
|
// core.Option{Key: "version", Value: "1.0.0"},
|
|
// ))
|
|
type App struct {
|
|
Name string
|
|
Version string
|
|
Description string
|
|
Filename string
|
|
Path string
|
|
Runtime any // GUI runtime (e.g., Wails App). Nil for CLI-only.
|
|
}
|
|
|
|
// New creates an App from Options.
|
|
//
|
|
// app := core.App{}.New(core.NewOptions(
|
|
// core.Option{Key: "name", Value: "myapp"},
|
|
// core.Option{Key: "version", Value: "1.0.0"},
|
|
// ))
|
|
func (a App) New(opts Options) App {
|
|
if name := opts.String("name"); name != "" {
|
|
a.Name = name
|
|
}
|
|
if version := opts.String("version"); version != "" {
|
|
a.Version = version
|
|
}
|
|
if desc := opts.String("description"); desc != "" {
|
|
a.Description = desc
|
|
}
|
|
if filename := opts.String("filename"); filename != "" {
|
|
a.Filename = filename
|
|
}
|
|
return a
|
|
}
|
|
|
|
// Find locates a program on PATH and returns a Result containing the App.
|
|
// Uses os.Stat to search PATH directories — no os/exec dependency.
|
|
//
|
|
// r := core.App{}.Find("node", "Node.js")
|
|
// if r.OK { app := r.Value.(*App) }
|
|
func (a App) Find(filename, name string) Result {
|
|
// If filename contains a separator, check it directly
|
|
if Contains(filename, string(os.PathSeparator)) {
|
|
abs, err := filepath.Abs(filename)
|
|
if err != nil {
|
|
return Result{err, false}
|
|
}
|
|
if isExecutable(abs) {
|
|
return Result{&App{Name: name, Filename: filename, Path: abs}, true}
|
|
}
|
|
return Result{E("app.Find", Concat(filename, " not found"), nil), false}
|
|
}
|
|
|
|
// Search PATH
|
|
pathEnv := os.Getenv("PATH")
|
|
if pathEnv == "" {
|
|
return Result{E("app.Find", "PATH is empty", nil), false}
|
|
}
|
|
for _, dir := range Split(pathEnv, string(os.PathListSeparator)) {
|
|
candidate := filepath.Join(dir, filename)
|
|
if isExecutable(candidate) {
|
|
abs, err := filepath.Abs(candidate)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
return Result{&App{Name: name, Filename: filename, Path: abs}, true}
|
|
}
|
|
}
|
|
return Result{E("app.Find", Concat(filename, " not found on PATH"), nil), false}
|
|
}
|
|
|
|
// isExecutable checks if a path exists and is executable.
|
|
func isExecutable(path string) bool {
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
// Regular file with at least one execute bit
|
|
return !info.IsDir() && info.Mode()&0111 != 0
|
|
}
|